-- MojoSetup; a portable, flexible installation application.
--
-- Please see the file LICENSE.txt in the source's root directory.
--
--  This file written by Ryan C. Gordon.


-- !!! FIXME: go through the schema and ditch loki_setup things we don't want.

-- These are various things that need to be exposed to Lua, or are just
--  better written in Lua than C. All work will be done in the "MojoSetup"
--  table (for generic functionality) or the "Setup" table (for config file
--  elements), so the rest of the namespace is available to end-user code,
--  minus what the Lua runtime claims for itself.
--
-- This file is loaded and executed at MojoSetup startup. The Base Archive,
--  GUI, and Lua are initialized, the MojoSetup table is created and has some
--  initial CFunctions and such inserted. The localization script runs, and
--  then this code executes to do the heavy lifting. All Lua state should be
--  sane for the rest of the app once this script successfully completes.

local _ = MojoSetup.translate

-- Set up the garbage counter. Things that are done in possibly long-running
--  loops that create a lot of junk can optionally set a threshold and call
--  MojoSetup.incrementgarbagecounter() on each iteration. Whenever you hit
--  the threshold, garbage collection runs and resets the counter. This keeps
--  memory usage from spiraling out of control for pathological cases.
-- Note that the counter resets in MojoSetup.collectgarbage() itself, so you
--  don't have to worry about the counter accumulating and firing off extra
--  unnecessary collections if you happen to hit a loop at the wrong time.
MojoSetup.garbagecounter = 0
MojoSetup.garbagethreshold = 500

function MojoSetup.incrementgarbagecount()
    MojoSetup.garbagecounter = MojoSetup.garbagecounter + 1
    if MojoSetup.garbagecounter >= MojoSetup.garbagethreshold then
        MojoSetup.collectgarbage()
    end
end

-- Returns three elements: protocol, host, path
function MojoSetup.spliturl(url)
    return string.match(url, "^(.+://)(.-)/(.*)")
end


-- This is MojoSetup.stringtofile() for string tables.
function MojoSetup.stringtabletofile(t, dest, perms, len, callback)
    -- !!! FIXME: We could do this entirely in C to avoid a concatenated
    -- !!! FIXME:  string on the Lua heap.
    return MojoSetup.stringtofile(table.concat(t), dest, perms, len, callback)
end


-- This is handy for debugging.
function MojoSetup.dumptable(tabname, tab, depth)
    if depth == nil then  -- first call, before any recursion?
        local loglevel = MojoSetup.info.loglevel
        if (loglevel ~= "debug") and (loglevel ~= "everything") then
            return  -- don't spend time on this if there's no output...
        end
        depth = 1
    end

    if tabname ~= nil then
        if tab == nil then
            MojoSetup.logdebug(tabname .. " = nil")
            return
        else
            MojoSetup.logdebug(tabname .. " = {")
        end
    end

    local depthstr = ""
    for i=1,(depth*4) do
        depthstr = depthstr .. " "
    end

    if tab.MOJOSETUP_DUMPTABLE_ITERATED then
        MojoSetup.logdebug(depthstr .. "(...circular reference...)")
    else
        tab.MOJOSETUP_DUMPTABLE_ITERATED = true
        for k,v in pairs(tab) do
            if type(v) == "table" then
                MojoSetup.logdebug(depthstr .. tostring(k) .. " = {")
                MojoSetup.dumptable(nil, v, depth + 1)
                MojoSetup.logdebug(depthstr .. "}")
            else
                if k ~= "MOJOSETUP_DUMPTABLE_ITERATED" then
                    MojoSetup.logdebug(depthstr .. tostring(k) .. " = " .. tostring(v))
                end
            end
        end
        tab.MOJOSETUP_DUMPTABLE_ITERATED = nil
    end

    if tabname ~= nil then
        MojoSetup.logdebug("}")
    end
end


-- Our namespace for config API...
Setup = {}

local function schema_assert(test, fnname, elem, errstr)
    if not test then
        local msg = MojoSetup.format(_("BUG: Config %0 %1"),
                                     fnname .. "::" .. elem, errstr)
        MojoSetup.fatal(msg)
    end
end

local function mustExist(fnname, elem, val)
    schema_assert(val ~= nil, fnname, elem, _("must be explicitly specified"))
end

local function mustBeSomething(fnname, elem, val, elemtype)
    -- Can be nil...please use mustExist if this is a problem!
    if val ~= nil then
        if type(val) ~= elemtype then
            local msg = MojoSetup.format(_("must be %0"),
                                         MojoSetup.translate(elemtype))
            schema_assert(false, fnname, elem, msg)
        end
    end
end

local function mustBeString(fnname, elem, val)
    mustBeSomething(fnname, elem, val, "string")
end

local function mustBeBool(fnname, elem, val)
    mustBeSomething(fnname, elem, val, "boolean")
end

local function mustBeNumber(fnname, elem, val)
    mustBeSomething(fnname, elem, val, "number")
end

local function mustBeFunction(fnname, elem, val)
    mustBeSomething(fnname, elem, val, "function")
end

local function mustBeTable(fnname, elem, val)
    mustBeSomething(fnname, elem, val, "table")
end

local function cantBeEmpty(fnname, elem, val)
    -- Can be nil...please use mustExist if this is a problem!
    if val ~= nil then
        schema_assert(val ~= "", fnname, elem, _("can't be empty string"))
    end
end

local function mustBeStringOrTableOfStrings(fnname, elem, val)
    -- Can be nil...please use mustExist if this is a problem!
    if val ~= nil then
        local err = _("must be string or table of strings")
        if type(val) == "string" then
            val = { val }
        end
        schema_assert(type(val) == "table", fnname, elem, err)
        for k,v in pairs(val) do
            schema_assert(type(v) == "string", fnname, elem, err)
        end
    end
end

local function mustBeStringOrNumber(fnname, elem, val)
    -- Can be nil...please use mustExist if this is a problem!
    if val ~= nil then
        local t = type(val)
        schema_assert((t == "string") or (t == "number"), fnname, elem,
                        _("must be a string or number"))
    end
end

local function mustBeUrl(fnname, elem, val)
    mustBeString(fnname, elem, val)
    cantBeEmpty(fnname, elem, val)
    if (val ~= nil) then
        local prot,host,path = MojoSetup.spliturl(val)
        schema_assert(prot ~= nil,fnname,elem,_("URL doesn't have protocol"))
        schema_assert(host ~= nil,fnname,elem,_("URL doesn't have host"))
        schema_assert(path ~= nil,fnname,elem,_("URL doesn't have path"))
        prot = string.gsub(prot, "://$", "")
        local supported = MojoSetup.info.supportedurls[prot]
        schema_assert(supported,fnname,elem,_("URL protocol is unsupported"))
    end
end

local function mustBeSplashPosition(fnname, elem, val)
    mustBeString(fnname, elem, val)
    local valid = (val == nil) or (val == "right") or (val == "bottom") or
                  (val == "left") or (val == "top") or (val == "background");
    schema_assert(valid, fnname, elem, _("Splash position is invalid"))
end

local function mustBePerms(fnname, elem, val)
    mustBeString(fnname, elem, val)
    local valid = MojoSetup.isvalidperms(val)
    schema_assert(valid, fnname, elem, _("Permission string is invalid"))
end

local function mustBeValidProductKeyFormat(fnname, elem, val)
    -- Can be nil...please use mustExist if this is a problem!
    if val ~= nil then
        mustBeString(fnname, elem, val)
        cantBeEmpty(fnname, elem, val)
        local valid = (string.match(val, "^[X#%*%? %-]*$") ~= nil)
        schema_assert(valid, fnname, elem, _("invalid string"))
    end
end

local function sanitize(fnname, tab, elems)
    mustBeTable(fnname, "", tab)
    tab._type_ = string.lower(fnname) .. "s"   -- "Eula" becomes "eulas".
    for i,elem in ipairs(elems) do
        local child = elem[1]
        local defval = elem[2]

        if tab[child] == nil and defval ~= nil then
            tab[child] = defval
        end
        local j = 3
        while elem[j] do
            elem[j](fnname, child, tab[child])  -- will assert on problem.
            j = j + 1
        end
    end

    local notvaliderr = _("is not a valid property")
    for k,v in pairs(tab) do
        local found = false
        if k == "_type_" then
            found = true
        elseif (type(k) == "number") and (type(v) == "table") then
            found = true
        else
            for i,elem in ipairs(elems) do
                local child = elem[1]
                if (child == k) then
                    found = true
                    break
                end
            end
        end
        schema_assert(found, fnname, k, notvaliderr)
    end

    return tab
end

local function reform_schema_table(tab)
    local killlist = {}
    for k,v in pairs(tab) do
        local ktype = type(k)
        local vtype = type(v)
        if (ktype == "number") and (vtype == "table") and (v._type_ ~= nil) then
            -- add element to proper named array.
            local typestr = v._type_
            v._type_ = nil
            MojoSetup.logdebug("schema: reforming '" .. typestr .. "', '" .. k .. "'")
            if tab[typestr] == nil then
                tab[typestr] = { v }
            else
                table.insert(tab[typestr], v)
            end
            -- can't just set tab[k] to nil here, since it screws up pairs()...
            killlist[#killlist+1] = k
        elseif vtype == "table" then
            tab[k] = reform_schema_table(v)
        end
    end

    for i,v in ipairs(killlist) do
        tab[v] = nil
    end

    return tab
end


-- Actual schema elements are below...

function Setup.Package(tab)
    -- !!! FIXME: allow_uninstall
    -- !!! FIXME: write_manifest
    -- !!! FIXME: allow_uninstall must check write_manifest, write_manifest
    -- !!! FIXME:  must check for Lua parser support...or something like that.

    tab = sanitize("Package", tab,
    {
        { "vendor", nil, mustExist, mustBeString, cantBeEmpty },
        { "id", nil, mustExist, mustBeString, cantBeEmpty },
        { "disabled", nil, mustBeBool },
        { "description", nil, mustExist, mustBeString, cantBeEmpty },
        { "version", nil, mustExist, mustBeString, cantBeEmpty },
        { "destination", nil, mustBeString, cantBeEmpty },
        { "dataprefix", "data/", mustBeString},
        { "recommended_destinations", nil, mustBeStringOrTableOfStrings },
        { "recommended_full_destinations", nil, mustBeStringOrTableOfStrings },
        { "precheck", nil, mustBeFunction },
        { "checkdst", nil, mustBeFunction },
        { "preflight", nil, mustBeFunction },
        { "preinstall", nil, mustBeFunction },
        { "postinstall", nil, mustBeFunction },
        { "splash", nil, mustBeString, cantBeEmpty },
        { "splashpos", nil, mustBeSplashPosition },
        { "url", nil, mustBeString, cantBeEmpty },
        { "once", true, mustBeBool },
        { "category", "Games", mustBeString, cantBeEmpty },
        { "promptoverwrite", true, mustBeBool },
        { "binarypath", nil, mustBeString, cantBeEmpty },
        { "updateurl", nil, mustBeUrl },
        { "superuser", false, mustBeBool },
        { "write_manifest", true, mustBeBool },
        { "support_uninstall", true, mustBeBool },
        { "preuninstall", nil, mustBeFunction },
        { "postuninstall", nil, mustBeFunction },
        { "postexec", nil, mustBeString},
        { "delete_error_is_fatal", true, mustBeBool}
    })

    if MojoSetup.installs == nil then
        MojoSetup.installs = {}
    end

    tab._type_ = nil
    tab = reform_schema_table(tab)
    table.insert(MojoSetup.installs, tab)
    return tab

--[[
 promptbinaries When set to "yes", setup will create a checkbox
                to allow the user whether or not to create 
                a symbolic link to the binaries.

                This setting has no effect if nobinaries is "yes".

 meta       When this attribute is set to "yes", then setup will act as
            a meta-installer, i.e. it will allow the user to select a
            product and set-up will respawn itself for the selected
            product installation.
            See the section "About Meta-Installation" below.

 component  When this attribute is present, its value is the name of the component
            that will created by this installer. This means that the files will be added
            to an existing installation of the product, and that basic configuration parameters
            such as the installation path will be used from the previous installation.
            Currently setup is not able to update an existing component, thus if the component
            is detected as already installed the installation will fail.
            Important: This attribute can't be mixed with <component> elements.

 appbundle  (CARBON ONLY) If this is "yes", the destination folder does not include the product
            name as part of the path.  An application bundle is typically installed much like
            a single file would be...and so is treated as such.
            
            The app bundle path usually specified in the "path" attribute of the "files" element,
            or it is specified in the archives directly.

 manpages   If set to "yes", then the user will be prompted for the install pages installation path.
            Should be used when using the MANPAGE element described below.

 appbundleid  (CARBON ONLY) This string means that you are installing new files into an existing
              Application Bundle. If the bundle isn't found, the installation aborts, otherwise, all
              files are added relative to the base of the app bundle. The string specified here is
              the app bundle's CFBundleIdentifier entry in its Info.plist file. LaunchServices are
              used to find the bundle and the user can be prompted to manually locate the product
              if LaunchServices fails to find it. This is all MacOS-specific, and unrelated to the
              "component" attribute.

 appbundledesc (CARBON ONLY) This string is used with the "appbundleid" attribute...it's a human
               readable text describing the app bundle in question.
]]--

end

function Setup.Eula(tab)
    return sanitize("Eula", tab,
    {
        { "description", nil, mustExist, mustBeString, cantBeEmpty },
        { "source", nil, mustExist, mustBeString, cantBeEmpty },
        { "accept_not_needed", nil, mustBeBool },
    })
end

function Setup.Readme(tab)
    return sanitize("Readme", tab,
    {
        { "description", nil, mustExist, mustBeString, cantBeEmpty },
        { "source", nil, mustExist, mustBeString, cantBeEmpty },
    })
end

function Setup.ProductKey(tab)
    return sanitize("ProductKey", tab,
    {
        { "description", nil, mustExist, mustBeString, cantBeEmpty },
        { "format", nil, mustBeValidProductKeyFormat },
        { "verify", nil, mustBeFunction },
        { "destination", nil, mustBeString, cantBeEmpty },
    })
end

function Setup.Media(tab)
    return sanitize("Media", tab,
    {
        { "id", nil, mustExist, mustBeString, cantBeEmpty },
        { "description", nil, mustExist, mustBeString, cantBeEmpty },
        { "uniquefile", nil, mustExist, mustBeString, cantBeEmpty },
    })
end

function Setup.File(tab)
    return sanitize("File", tab,
    {
        { "source", nil, mustBeUrl },
        { "destination", nil, mustBeString, cantBeEmpty },
        { "wildcards", nil, mustBeStringOrTableOfStrings },
        { "filter", nil, mustBeFunction },
        { "allowoverwrite", nil, mustBeBool },
        { "permissions", nil, mustBePerms },
    })
end

function Setup.Option(tab)
    return sanitize("Option", tab,
    {
        { "value", false, mustBeBool },
        { "required", false, mustBeBool },
        { "disabled", false, mustBeBool },
        { "bytes", nil, mustExist, mustBeNumber },
        { "description", nil, mustExist, mustBeString, cantBeEmpty },
        { "tooltip", nil, mustBeString, cantBeEmpty },
    })
end

function Setup.OptionGroup(tab)
    return sanitize("OptionGroup", tab,
    {
        { "disabled", nil, mustBeBool },
        { "description", nil, mustExist, mustBeString, cantBeEmpty },
        { "tooltip", nil, mustBeString, cantBeEmpty },
    })
end

function Setup.DesktopMenuItem(tab)
    return sanitize("DesktopMenuItem", tab,
    {
        { "disabled", nil, mustBeBool },
        { "name", nil, mustExist, mustBeString, cantBeEmpty },
        { "genericname", nil, mustExist, mustBeString, cantBeEmpty },
        { "tooltip", nil, mustExist, mustBeString, cantBeEmpty },
        { "builtin_icon", nil, mustBeBool },
        { "icon", nil, mustExist, mustBeString, cantBeEmpty },
        { "commandline", nil, mustExist, mustBeString, cantBeEmpty },
        { "workingdir", nil, mustBeString, cantBeEmpty },
        { "category", nil, mustExist, mustBeStringOrTableOfStrings },
        { "mimetype", nil, mustBeStringOrTableOfStrings },
    })
end

local function prepare_localization()
    -- Map some legacy language identifiers into updated equivalents.
    local lang_remap =
    {
        no = "nb",  -- "Norwegian" split into "Bokmal" (nb) and "Nynorsk" (nn)
    }

    local function sanity_check_localization_entry(str, translations)
        local maxval = -1;

        for val in string.gmatch(str, "%%.") do
            val = string.sub(val, 2)
            if string.match(val, "^[^%%0-9]$") ~= nil then
                MojoSetup.fatal("BUG: localization key ['" .. str .. "'] has invalid escape sequence.")
            end
            if val ~= "%" then
                local num = tonumber(val)
                if num > maxval then
                    maxval = num
                end
            end
        end

        for k,v in pairs(translations) do
            for val in string.gmatch(v, "%%.") do
                val = string.sub(val, 2)
                if string.match(val, "^[^%%0-9]$") ~= nil then
                    MojoSetup.fatal("BUG: '" .. k .. "' localization ['" .. v .. "'] has invalid escape sequence for translation of ['" .. str .. "'].")
                end
                if val ~= "%" then
                    if tonumber(val) > maxval then
                        MojoSetup.fatal("BUG: '" .. k .. "' localization ['" .. v .. "'] has escape sequence > max for translation of ['" .. str .. "'].")
                    end
                end
            end
        end
    end

    -- Build the translations table from the localizations table supplied in
    --  localizations.lua...
    if type(MojoSetup.localization) ~= "table" then
        MojoSetup.localization = nil
    end

    -- Merge the applocalization table into localization.
    if type(MojoSetup.applocalization) == "table" then
        if MojoSetup.localization == nil then
            MojoSetup.localization = {}
        end
        for k,v in pairs(MojoSetup.applocalization) do
            if MojoSetup.localization[k] == nil then
                MojoSetup.localization[k] = v  -- just take the whole table as-is.
            else
                -- This can add or overwrite entries...
                for lang,str in pairs(v) do
                    MojoSetup.localization[k][lang] = str
                end
            end
        end
    end
    MojoSetup.applocalization = nil  -- done with this; garbage collect it.

    if MojoSetup.localization ~= nil then
        local at_least_one = false
        local locale = MojoSetup.info.locale
        local lang = string.gsub(locale, "_%w+", "", 1)  -- make "en_US" into "en"

        if lang_remap[lang] ~= nil then
            lang = lang_remap[lang]
        end

        MojoSetup.translations = {}
        for k,v in pairs(MojoSetup.localization) do
            if MojoSetup.translations[k] ~= nil then
                MojoSetup.fatal("BUG: Duplicate localization key ['" .. k .. "']")
            end
            if type(v) == "table" then
                sanity_check_localization_entry(k, v)
                if v[locale] ~= nil then
                    MojoSetup.translations[k] = v[locale]
                    at_least_one = true
                elseif v[lang] ~= nil then
                    MojoSetup.translations[k] = v[lang]
                    at_least_one = true
                end
            end
        end

        -- Delete the table if there's no actual useful translations for this run.
        if (not at_least_one) then
            MojoSetup.translations = nil
        end

        -- This is eligible for garbage collection now. We're done with it.
        MojoSetup.localization = nil
    end
end


-- Mainline...

MojoSetup.loginfo("MojoSetup Lua initialization at " .. MojoSetup.date())
MojoSetup.loginfo("buildver: " .. MojoSetup.info.buildver)
MojoSetup.loginfo("locale: " .. MojoSetup.info.locale)
MojoSetup.loginfo("platform: " .. MojoSetup.info.platform)
MojoSetup.loginfo("arch: " .. MojoSetup.info.arch)
MojoSetup.loginfo("ostype: " .. MojoSetup.info.ostype)
MojoSetup.loginfo("osversion: " .. MojoSetup.info.osversion)
MojoSetup.loginfo("ui: " .. MojoSetup.info.ui)
MojoSetup.loginfo("loglevel: " .. MojoSetup.info.loglevel)

MojoSetup.loginfo("command line:")
for i,v in ipairs(MojoSetup.info.argv) do
    MojoSetup.loginfo("  " .. i .. ": " .. v)
end

--MojoSetup.loginfo(MojoSetup.info.license)
--MojoSetup.loginfo(MojoSetup.info.lualicense)

-- These scripts are optional, but hopefully exist...
MojoSetup.runfile("localization")
MojoSetup.runfile("app_localization")
prepare_localization()

-- okay, we're initialized!

-- end of mojosetup_init.lua ...

