--!A cross-platform build utility based on Lua
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- Copyright (C) 2015-present, Xmake Open Source Community.
--
-- @author      ruki
-- @file        main.lua
--

-- imports
import("core.base.option")
import("core.base.semver")
import("core.base.hashset")
import("lib.detect.find_tool")
import("lib.detect.find_file")
import("utils.archive")
import("private.action.require.impl.packagenv")
import("private.action.require.impl.install_packages")
import(".batchcmds")

-- get the rpmbuild
function _get_rpmbuild()

    -- enter the environments of rpmbuild
    local oldenvs = packagenv.enter("rpm")

    -- find rpmbuild
    local packages = {}
    local rpmbuild = find_tool("rpmbuild")
    if not rpmbuild then
        table.join2(packages, install_packages("rpm"))
    end

    -- enter the environments of installed packages
    for _, instance in ipairs(packages) do
        instance:envs_enter()
    end

    -- we need to force detect and flush detect cache after loading all environments
    if not rpmbuild then
        rpmbuild = find_tool("rpmbuild", {force = true})
    end
    assert(rpmbuild, "rpmbuild not found!")
    return rpmbuild, oldenvs
end

-- get archive file
function _get_archivefile(package)
    return path.absolute(path.join(package:builddir(), package:basename() .. ".tar.gz"))
end

-- translate the file path
function _translate_filepath(package, filepath)
    return filepath:replace(package:install_rootdir(), "%{buildroot}/%{_exec_prefix}", {plain = true})
end

-- get install command
function _get_customcmd(package, installcmds, cmd)
    local opt = cmd.opt or {}
    local kind = cmd.kind
    if kind == "cp" then
        local srcfiles = os.files(cmd.srcpath)
        for _, srcfile in ipairs(srcfiles) do
            -- the destination is directory? append the filename
            local dstfile = _translate_filepath(package, cmd.dstpath)
            if #srcfiles > 1 or path.islastsep(dstfile) then
                if opt.rootdir then
                    dstfile = path.join(dstfile, path.relative(srcfile, opt.rootdir))
                else
                    dstfile = path.join(dstfile, path.filename(srcfile))
                end
            end
            table.insert(installcmds, string.format("install -Dpm0644 \"%s\" \"%s\"", path.absolute(srcfile), dstfile))
        end
    elseif kind == "rm" then
        local filepath = _translate_filepath(package, cmd.filepath)
        table.insert(installcmds, string.format("rm -f \"%s\"", filepath))
    elseif kind == "rmdir" then
        local dir = _translate_filepath(package, cmd.dir)
        table.insert(installcmds, string.format("rm -rf \"%s\"", dir))
    elseif kind == "mv" then
        local srcpath = _translate_filepath(package, cmd.srcpath)
        local dstpath = _translate_filepath(package, cmd.dstpath)
        table.insert(installcmds, string.format("mv \"%s\" \"%s\"", srcfile, dstfile))
    elseif kind == "cd" then
        local dir = _translate_filepath(package, cmd.dir)
        table.insert(installcmds, string.format("cd \"%s\"", dir))
    elseif kind == "mkdir" then
        local dir = _translate_filepath(package, cmd.dir)
        table.insert(installcmds, string.format("mkdir -p \"%s\"", dir))
    elseif cmd.program then
        local argv = {}
        for _, arg in ipairs(cmd.argv) do
            local arg = arg
            if path.instance_of(arg) then
                arg = arg:clone():set(_translate_filepath(package, arg:rawstr())):str()
            elseif path.is_absolute(arg) then
                arg = _translate_filepath(package, arg)
            end
            table.insert(argv, arg)
        end
        table.insert(installcmds, string.format("%s", os.args(table.join(cmd.program, argv))))
    end
end

-- get build commands
function _get_buildcmds(package, buildcmds, cmds)
    for _, cmd in ipairs(cmds) do
        _get_customcmd(package, buildcmds, cmd)
    end
end

-- get install commands
function _get_installcmds(package, installcmds, cmds)
    for _, cmd in ipairs(cmds) do
        _get_customcmd(package, installcmds, cmd)
    end
end

-- get specvars
function _get_specvars(package)
    local specvars = table.clone(package:specvars())
    specvars.PACKAGE_ARCHIVEFILE = path.filename(_get_archivefile(package))
    local datestr = os.iorunv("date", {"+%a %b %d %Y"}, {envs = {LC_TIME = "en_US"}})
    if datestr then
        datestr = datestr:trim()
    end
    specvars.PACKAGE_PREFIXDIR = package:prefixdir() or ""
    specvars.PACKAGE_DATE = datestr or ""
    specvars.PACKAGE_INSTALLCMDS = function ()
        local prefixdir = package:get("prefixdir")
        package:set("prefixdir", nil)
        local installcmds = {}
        _get_installcmds(package, installcmds, batchcmds.get_installcmds(package):cmds())
        for _, component in table.orderpairs(package:components()) do
            if component:get("default") ~= false then
                _get_installcmds(package, installcmds, batchcmds.get_installcmds(component):cmds())
            end
        end
        package:set("prefixdir", prefixdir)
        return table.concat(installcmds, "\n")
    end
    specvars.PACKAGE_BUILDCMDS = function ()
        local buildcmds = {}
        _get_buildcmds(package, buildcmds, batchcmds.get_buildcmds(package):cmds())
        return table.concat(buildcmds, "\n")
    end
    specvars.PACKAGE_BUILDREQUIRES = function ()
        local requires = {}
        local buildrequires = package:get("buildrequires")
        if buildrequires then
            for _, buildrequire in ipairs(buildrequires) do
                table.insert(requires, "BuildRequires: " .. buildrequire)
            end
        else
            local programs = hashset.new()
            for _, cmd in ipairs(batchcmds.get_buildcmds(package):cmds()) do
                local program = cmd.program
                if program then
                    programs:insert(program)
                end
            end
            local map = {
                xmake = "xmake",
                cmake = "cmake",
                make = "make"
            }
            for _, program in programs:keys() do
                local requirename = map[program]
                if requirename then
                    table.insert(requires, "BuildRequires: " .. requirename)
                end
            end
            if #requires > 0 then
                table.insert(requires, "BuildRequires: gcc")
                table.insert(requires, "BuildRequires: gcc-c++")
            end
        end
        return table.concat(requires, "\n")
    end
    return specvars
end

-- pack srpm package
function _pack_srpm(rpmbuild, package)

    -- ensure prefixdir
    local prefixdir = package:get("prefixdir")
    if not prefixdir then
        prefixdir = package:name() .. "-" .. package:version()
        package:set("prefixdir", prefixdir)
    end

    -- install the initial specfile
    local specfile = path.join(package:builddir(), package:basename() .. ".spec")
    if not os.isfile(specfile) then
        local specfile_template = package:get("specfile") or path.join(os.programdir(), "scripts", "xpack", "srpm", "srpm.spec")
        os.cp(specfile_template, specfile, {writeable = true})
    end

    -- replace variables in specfile
    -- and we need to avoid `attempt to yield across a C-call boundary` in io.gsub
    local specvars = _get_specvars(package)
    local pattern = package:extraconf("specfile", "pattern") or "%${([^\n]-)}"
    local specvars_names = {}
    local specvars_values = {}
    io.gsub(specfile, "(" .. pattern .. ")", function(_, name)
        table.insert(specvars_names, name)
    end)
    for _, name in ipairs(specvars_names) do
        local name = name:trim()
        if specvars_values[name] == nil then
            local value = specvars[name]
            if type(value) == "function" then
                value = value()
            end
            if value ~= nil then
                dprint("  > replace %s -> %s", name, value)
            end
            if type(value) == "table" then
                dprint("invalid variable value", value)
            end
            specvars_values[name] = value
        end
    end
    io.gsub(specfile, "(" .. pattern .. ")", function(_, name)
        name = name:trim()
        return specvars_values[name]
    end)

    -- archive source files
    local srcfiles, dstfiles = package:sourcefiles()
    for idx, srcfile in ipairs(srcfiles) do
        os.vcp(srcfile, dstfiles[idx])
    end
    for _, component in table.orderpairs(package:components()) do
        if component:get("default") ~= false then
            local srcfiles, dstfiles = component:sourcefiles()
            for idx, srcfile in ipairs(srcfiles) do
                os.vcp(srcfile, dstfiles[idx])
            end
        end
    end

    -- archive install files
    local rootdir = package:source_rootdir()
    local oldir = os.cd(rootdir)
    local archivefiles = os.files("**")
    os.cd(oldir)
    local archivefile = _get_archivefile(package)
    os.tryrm(archivefile)
    archive.archive(archivefile, archivefiles, {curdir = rootdir, compress = "best"})

    -- pack srpm package
    os.vrunv(rpmbuild, {"-bs", specfile,
        "--define", "_topdir " .. package:builddir(),
        "--define", "_sourcedir " .. package:builddir(),
        "--define", "_srcrpmdir " .. package:outputdir()})

    -- pack rpm package
    if package:format() == "rpm" then
        local srpmfile = find_file("*.src.rpm", package:outputdir())
        if srpmfile then
            os.vrunv(rpmbuild, {"--rebuild", srpmfile, "--define", "_rpmdir " .. package:outputdir()})
        end
    end
end

function main(package)

    if not is_host("linux") then
        return
    end

    cprint("packing %s", package:outputfile())

    -- get rpmbuild
    local rpmbuild, oldenvs = _get_rpmbuild()

    -- pack srpm package
    _pack_srpm(rpmbuild.program, package)

    -- done
    os.setenvs(oldenvs)
end
