#!/usr/local/bin/lua
--[[
  udreportcnv.lua - UltraDefrag report converter.
  Converts Lua reports to HTML and other formats.
  Copyright (c) 2018-2024 Green Gate Systems, LLC.

  ALL RIGHTS RESERVED. YOU MAY NOT DISTRIBUTE OR MODIFY THIS SOFTWARE.

  THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EITHER
  EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF
  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK
  AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD
  THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY
  SERVICING, REPAIR OR CORRECTION.

  IN NO EVENT WILL THE AUTHORS AND COPYRIGHT HOLDERS BE LIABLE TO YOU
  FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
  DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING
  BUT NOT LIMITED TO LOSS OF DATA OR A FAILURE OF THE PROGRAM TO OPERATE
  WITH ANY OTHER PROGRAMS).
--]]

usage = [[
USAGE: lua udreportcnv.lua {path to lua report} [-v] [--theme=<light|dark|system>]
]]

theme = "system"
themeStyle = ""
themeSwitch = ""

-------------------------------------------------------------------------------
-- Localization Strings
-------------------------------------------------------------------------------

FRAGMENTED_FILES_ON = "Fragmented files on"
VISIT_HOMEPAGE      = "Visit our Homepage"
VIEW_REPORT_OPTIONS = "View report options"
POWERED_BY_LUA      = "Powered by Lua"
FRAGMENTS           = "Fragments"
SIZE                = "Size"
FILENAME            = "Filename"
COMMENT             = "Comment"
STATUS              = "Status"
LOCKED              = "locked"
MOVE_FAILED         = "move failed"
INVALID             = "invalid"
HELP                = "Help"

-------------------------------------------------------------------------------
-- Auxiliary Procedures
-------------------------------------------------------------------------------

-- converts number of bytes to a human readable format
function hrsize(n)
    local suffixes = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
    local i = 1
    while n >= 1024 do
        n = n / 1024
        i = i + 1
    end
    -- round up
    local m = math.floor(n)
    if n >= (m + 0.5) then m = m + 1 end
    return m .. " " .. suffixes[i]
end

function display_report(path)
    local ret, msg
    if os.shellexec then
        ret, msg = os.shellexec(path,"open")
        if ret <= 32 then error(msg) end
    else
        if os.execute("cmd.exe /C " .. path) ~= 0 then
            error("Cannot execute cmd.exe /C " .. path .. " command!")
        end
    end
end

-------------------------------------------------------------------------------
-- Internationalization Procedures
-------------------------------------------------------------------------------

function get_localization_strings()
    local BOM = string.char(0xEF,0xBB,0xBF)
    
    f = io.open(instdir .. "\\reports.lng","r")
    if not f then return end
    for line in f:lines() do
        local pair, key, value
        i, j, pair = string.find(line,string.format("^%s(.-)$",BOM))
        if not pair then pair = line end
        i, j, key, value = string.find(pair,"^%s*(.-)%s*=%s*(.-)%s*$")
        if key and value then
            if _G[key] then _G[key] = string.gsub(value,"\\n","\n") end
        end
    end
    f:close()
end

-------------------------------------------------------------------------------
-- Plain Text Output Procedures
-------------------------------------------------------------------------------

-- Plain text reports can be used in batch scripts to extract
-- information from. So, let's avoid their localization.

function write_text_header(f)
    local formatted_time = ""
    
    if computer_name ~= "" then
        computer_name = computer_name .. ': '
    end
    
    -- format time according to the current locale
    if current_time then
        formatted_time = os.date("%c",os.time(current_time))
    end
    
    -- write byte order mark
    f:write(string.char(0xEF,0xBB,0xBF))

    f:write(";---------------------------------------------------------------------------------------------\n")
    f:write("; ", computer_name, "Fragmented files on ", volume_letter, ": [", formatted_time, "]\n;\n")
    f:write("; Fragments    Filesize  Comment      Status    Filename\n")
    f:write(";---------------------------------------------------------------------------------------------\n")
    f:write("\n")
end

function write_main_table(f)
    for i, file in ipairs(files) do
        f:write(string.format("%11u%12s%9s%12s    ",
            file.fragments, 
            hrsize(file.size),
            file.comment,
            file.status)
        )
        f:write(string.gsub(file.path,"/","\\"),"\n")
    end
end

function build_text_report()
    local filename = string.gsub(report_path,"%.luar$","%.txt")
    local f = assert(io.open(filename,"w"))
    write_text_header(f)
    write_main_table(f)
    f:close()
    return filename
end

-------------------------------------------------------------------------------
-- HTML Output Procedures
-------------------------------------------------------------------------------

-- HTML reports are intended to be opened in a web
-- browser. So, let's use localized strings there.

formatted_time = ""

header = [[
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="format-detection" content="telephone=no">

    <title>$computer_name$FRAGMENTED_FILES_ON $volume_letter: [$formatted_time]</title>

    <link rel="stylesheet" href="../scripts/sanitize.css">
    $themeStyle
    <link rel="stylesheet" href="../scripts/udreport.css">
    <link rel="stylesheet" href="../scripts/udreport-custom.css">
    <link rel="icon" type="image/png" sizes="16x16" href="../scripts/favicon-16x16.png">
    <link rel="icon" type="image/png" sizes="32x32" href="../scripts/favicon-32x32.png">
  </head>
  <body>
    <noscript>
        <div class="noscript-banner">
            <div class="noscript-message">
                File fragmentation reports require <a href="https://www.computerhope.com/issues/ch000891.htm" target="_blank">JavaScript enabled</a> in your browser!
            </div>
        </div>
    </noscript>
    <header>
      <div class="header-content">
        <div class="site-title">
          <a href="https://ultradefrag.net" target="_blank">UltraDefrag</a>
        </div>

        <form class="search" action="javascript:void(0);">
          <input type="search" name="q" placeholder="Search...">
          <button title="Search">
            <!-- https://fontawesome.com/v5.15/icons/search?style=solid -->
            <svg class="search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"></path></svg>
          </button>
        </form>

        <div class="top-menu">
          <div class="top-menu-links">
            $themeSwitch
            <a href="https://ultradefrag.net/support/basic-usage/first-steps-with-ultradefrag/#file-fragmentation-reports" target="_blank" title="$HELP"><svg class="help-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 76c48.1 0 93.3 18.7 127.3 52.7S436 207.9 436 256s-18.7 93.3-52.7 127.3S304.1 436 256 436c-48.1 0-93.3-18.7-127.3-52.7S76 304.1 76 256s18.7-93.3 52.7-127.3S207.9 76 256 76m0-28C141.1 48 48 141.1 48 256s93.1 208 208 208 208-93.1 208-208S370.9 48 256 48z"/><path d="M256.7 160c37.5 0 63.3 20.8 63.3 50.7 0 19.8-9.6 33.5-28.1 44.4-17.4 10.1-23.3 17.5-23.3 30.3v7.9h-34.7l-.3-8.6c-1.7-20.6 5.5-33.4 23.6-44 16.9-10.1 24-16.5 24-28.9s-12-21.5-26.9-21.5c-15.1 0-26 9.8-26.8 24.6H192c.7-32.2 24.5-54.9 64.7-54.9zm-26.3 171.4c0-11.5 9.6-20.6 21.4-20.6 11.9 0 21.5 9 21.5 20.6s-9.6 20.6-21.5 20.6-21.4-9-21.4-20.6z"/></svg></a>
          </div>
        </div>
      </div>
    </header>

    <div class="heading">
      <h3>$computer_name$FRAGMENTED_FILES_ON $volume_letter: ($formatted_time)</h3>
      <div class="pager"></div>
    </div>

    <table class="fragmented-files-list">
      <thead>
        <tr>
          <td>
            <div>
              <div>$FRAGMENTS</div>
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M27.66 224h264.7c24.6 0 36.89-29.78 19.54-47.12l-132.3-136.8c-5.406-5.406-12.47-8.107-19.53-8.107c-7.055 0-14.09 2.701-19.45 8.107L8.119 176.9C-9.229 194.2 3.055 224 27.66 224zM292.3 288H27.66c-24.6 0-36.89 29.77-19.54 47.12l132.5 136.8C145.9 477.3 152.1 480 160 480c7.053 0 14.12-2.703 19.53-8.109l132.3-136.8C329.2 317.8 316.9 288 292.3 288z"/></svg>
            </div>
          </td>
          <td>
            <div>
              <div>$SIZE</div>
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M27.66 224h264.7c24.6 0 36.89-29.78 19.54-47.12l-132.3-136.8c-5.406-5.406-12.47-8.107-19.53-8.107c-7.055 0-14.09 2.701-19.45 8.107L8.119 176.9C-9.229 194.2 3.055 224 27.66 224zM292.3 288H27.66c-24.6 0-36.89 29.77-19.54 47.12l132.5 136.8C145.9 477.3 152.1 480 160 480c7.053 0 14.12-2.703 19.53-8.109l132.3-136.8C329.2 317.8 316.9 288 292.3 288z"/></svg>
            </div>
          </td>
          <td>
            <div>
              <div>$FILENAME</div>
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M27.66 224h264.7c24.6 0 36.89-29.78 19.54-47.12l-132.3-136.8c-5.406-5.406-12.47-8.107-19.53-8.107c-7.055 0-14.09 2.701-19.45 8.107L8.119 176.9C-9.229 194.2 3.055 224 27.66 224zM292.3 288H27.66c-24.6 0-36.89 29.77-19.54 47.12l132.5 136.8C145.9 477.3 152.1 480 160 480c7.053 0 14.12-2.703 19.53-8.109l132.3-136.8C329.2 317.8 316.9 288 292.3 288z"/></svg>
            </div>
          </td>
          <td>
            <div>
              <div>$COMMENT</div>
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M27.66 224h264.7c24.6 0 36.89-29.78 19.54-47.12l-132.3-136.8c-5.406-5.406-12.47-8.107-19.53-8.107c-7.055 0-14.09 2.701-19.45 8.107L8.119 176.9C-9.229 194.2 3.055 224 27.66 224zM292.3 288H27.66c-24.6 0-36.89 29.77-19.54 47.12l132.5 136.8C145.9 477.3 152.1 480 160 480c7.053 0 14.12-2.703 19.53-8.109l132.3-136.8C329.2 317.8 316.9 288 292.3 288z"/></svg>
            </div>
          </td>
          <td>
            <div>
              <div>$STATUS</div>
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M27.66 224h264.7c24.6 0 36.89-29.78 19.54-47.12l-132.3-136.8c-5.406-5.406-12.47-8.107-19.53-8.107c-7.055 0-14.09 2.701-19.45 8.107L8.119 176.9C-9.229 194.2 3.055 224 27.66 224zM292.3 288H27.66c-24.6 0-36.89 29.77-19.54 47.12l132.5 136.8C145.9 477.3 152.1 480 160 480c7.053 0 14.12-2.703 19.53-8.109l132.3-136.8C329.2 317.8 316.9 288 292.3 288z"/></svg>
            </div>
          </td>
        </tr>
      </thead>
      <tbody>
      </tbody>
    </table>

    <div class="pager bottom-pager"></div>

    <nav class="scroll-to-top" title="Scroll to Top">
      <svg class="arrow-up-icon" viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"></path></svg>
    </nav>

    <script>
]]

footer = [[
    </script>
    <script src="../scripts/udsorting.js"></script>
  </body>
</html>
]]

function expand (s)
  s = string.gsub(s, "$([%w_]+)", function (n)
        return tostring(_G[n])
      end)
  return s
end

function write_file_status(f,file)
    if file.status == "locked" then
        f:write(LOCKED)
    elseif file.status == "move failed" then
        f:write(MOVE_FAILED)
    elseif file.status == "invalid" then
        f:write(INVALID)
    else
        f:write(file.status)
    end
end

function save_fragmented_files_list(f)
    f:write("const fragmented_files = [\n")

    for i, file in ipairs(files) do
        f:write("{")

        f:write("fragments: ", file.fragments, ", ")
        f:write("size: ", file.size, ", ")
        f:write("hrsize: \"", string.gsub(hrsize(file.size)," ","&nbsp;"), "\", ")
        f:write("path: \"", string.gsub(file.path,"/","\\\\"), "\", ")
        f:write("comment: \"", file.comment, "\", ")
        f:write("status: \"")
        write_file_status(f,file)
        f:write("\"")

        f:write("},\n")
    end

    f:write("];\n")
end

function build_html_report()
    local filename = string.gsub(report_path,"%.luar$","%.html")
    local f = assert(io.open(filename,"w"))

    if computer_name ~= "" then
        computer_name = computer_name .. ': '
    end
    
    -- format time according to the current locale
    if current_time then
        formatted_time = os.date("%c",os.time(current_time))
    end

    if theme == "light" then
        themeStyle = [[
            <style id="theme-style">
                :root {
                    --text-color: rgb(17 24 39); /* gray-900 */
                    --border-color: rgb(229 231 235); /* gray-200 */
                    --highlight-color: rgb(243 244 246); /* gray-100 */
                    --bg-color: white;
                }
            </style>
        ]]

        themeSwitch = [[
            <a id="theme-switch" href="javascript:setTheme('dark')" title="Light theme">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path fill="currentColor" stroke="currentColor" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"></path><path stroke="currentColor" d="M12 4v1M17.66 6.344l-.828.828M20.005 12.004h-1M17.66 17.664l-.828-.828M12 20.01V19M6.34 17.664l.835-.836M3.995 12.004h1.01M6 6l.835.836"></path></svg>
            </a>
        ]]
    elseif theme == "dark" then
        themeStyle = [[
            <style id="theme-style">
                :root {
                    --text-color: rgb(209 213 219); /* gray-300 */
                    --border-color: #494c50;
                    --highlight-color: #222;
                    --bg-color: #292a2d;
                }
            </style>
        ]]

        themeSwitch = [[
            <a id="theme-switch" href="javascript:setTheme('system')" title="Dark theme">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m17.715 15.15.95.316a1 1 0 0 0-1.445-1.185l.495.869ZM9 6.035l.846.534a1 1 0 0 0-1.14-1.49L9 6.035Zm8.221 8.246a5.47 5.47 0 0 1-2.72.718v2a7.47 7.47 0 0 0 3.71-.98l-.99-1.738Zm-2.72.718A5.5 5.5 0 0 1 9 9.5H7a7.5 7.5 0 0 0 7.5 7.5v-2ZM9 9.5c0-1.079.31-2.082.845-2.93L8.153 5.5A7.47 7.47 0 0 0 7 9.5h2Zm-4 3.368C5 10.089 6.815 7.75 9.292 6.99L8.706 5.08C5.397 6.094 3 9.201 3 12.867h2Zm6.042 6.136C7.718 19.003 5 16.268 5 12.867H3c0 4.48 3.588 8.136 8.042 8.136v-2Zm5.725-4.17c-.81 2.433-3.074 4.17-5.725 4.17v2c3.552 0 6.553-2.327 7.622-5.537l-1.897-.632Z"></path><path fill="currentColor" d="M17 3a1 1 0 0 1 1 1 2 2 0 0 0 2 2 1 1 0 1 1 0 2 2 2 0 0 0-2 2 1 1 0 1 1-2 0 2 2 0 0 0-2-2 1 1 0 1 1 0-2 2 2 0 0 0 2-2 1 1 0 0 1 1-1Z"></path></svg>
            </a>
        ]]
    else
        themeStyle = [[
            <style id="theme-style">
                :root {
                    --text-color: rgb(17 24 39); /* gray-900 */
                    --border-color: rgb(229 231 235); /* gray-200 */
                    --highlight-color: rgb(243 244 246); /* gray-100 */
                    --bg-color: white;
                }
                @media (prefers-color-scheme: dark) {
                    :root {
                        --text-color: rgb(209 213 219); /* gray-300 */
                        --border-color: #494c50;
                        --highlight-color: #222;
                        --bg-color: #292a2d;
                    }
                }
            </style>
        ]]

        themeSwitch = [[
            <a id="theme-switch" href="javascript:setTheme('light')" title="System theme">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linejoin="round"><path stroke="currentColor" d="M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6Z"></path><path stroke="currentColor" stroke-linecap="round" d="M14 15c0 3 2 5 2 5H8s2-2 2-5"></path></svg>
            </a>
        ]]
    end

    f:write(expand(header))
    save_fragmented_files_list(f)
    f:write(expand(footer))

    f:close()
    return filename
end

-------------------------------------------------------------------------------
-- The main code
-------------------------------------------------------------------------------
report_path = arg[1]
assert(report_path, usage)

instdir = string.gsub(report_path,"\\reports\\fraglist_.%.luar$","")
if instdir == report_path then instdir = ".." end

-- get report options
dofile(instdir .. "\\conf\\options.lua")

-- read the source file
dofile(report_path)

error_msg = [[
Reports produced by old versions of UltraDefrag
are no longer supported. Run the disk analysis again.
]]

-- check the report format version
if format_version == nil or format_version < 7 then
    error(error_msg)
end

-- read localization strings
get_localization_strings()

-- set color theme
if arg[2] == "--theme=light" or arg[3] == "--theme=light" then
    theme = "light"
end
if arg[2] == "--theme=dark" or arg[3] == "--theme=dark" then
    theme = "dark"
end

-- build file fragmentation reports
if produce_html_report == 1 then
    html_report_path = build_html_report()
end
if produce_plain_text_report == 1 then
    text_report_path = build_text_report()
end

-- display the report if requested
if arg[2] == "-v" then
    if produce_html_report == 1 then
        display_report(html_report_path)
    elseif produce_plain_text_report == 1 then
        display_report(text_report_path)
    end
end
