--!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.hashset")
import("core.project.project")
import("utils.binary.deplibs", {alias = "get_depend_libraries"})
import("utils.binary.rpath", {alias = "rpath_utils"})
import("private.utils.target", {alias = "target_utils"})

function _get_target_libdir(target, opt)
    if not opt.installdir then
        return target:libdir()
    end
    assert(opt.libdir, "opt.libdir is missing")
    return path.join(opt.installdir, opt.libdir)
end

function _get_target_bindir(target, opt)
    if not opt.installdir then
        return target:bindir()
    end
    assert(opt.bindir, "opt.bindir is missing")
    return path.join(opt.installdir, opt.bindir)
end

function _get_target_includedir(target, opt)
    if not opt.installdir then
        return target:includedir()
    end
    assert(opt.includedir, "opt.includedir is missing")
    return path.join(opt.installdir, opt.includedir)
end

-- copy file with symlinks
function _copy_file_with_symlinks(srcfile, outputdir)
    if os.islink(srcfile) then
        local srcfile_symlink = os.readlink(srcfile)
        if not path.is_absolute(srcfile_symlink) then
            srcfile_symlink = path.join(path.directory(srcfile), srcfile_symlink)
        end
        _copy_file_with_symlinks(srcfile_symlink, outputdir)
        os.vcp(srcfile, path.join(outputdir, path.filename(srcfile)), {symlink = true, force = true})
    else
        os.vcp(srcfile, path.join(outputdir, path.filename(srcfile)))
    end
end

-- install files
function _install_files(target)
    local srcfiles, dstfiles = target:installfiles()
    if srcfiles and dstfiles then
        for idx, srcfile in ipairs(srcfiles) do
            os.vcp(srcfile, dstfiles[idx])
        end
    end
    for _, dep in ipairs(target:orderdeps()) do
        local srcfiles, dstfiles = dep:installfiles(dep:installdir(), {interface = true})
        if srcfiles and dstfiles then
            for idx, srcfile in ipairs(srcfiles) do
                os.vcp(srcfile, dstfiles[idx])
            end
        end
    end
end

-- install headers
function _install_headers(target, opt)
    local srcheaders, dstheaders = target:headerfiles(_get_target_includedir(target, opt), {installonly = true})
    if srcheaders and dstheaders then
        for idx, srcheader in ipairs(srcheaders) do
            os.vcp(srcheader, dstheaders[idx])
        end
    end
    for _, dep in ipairs(target:orderdeps()) do
        local srcfiles, dstfiles = dep:headerfiles(_get_target_includedir(dep, opt), {installonly = true, interface = true})
        if srcfiles and dstfiles then
            for idx, srcfile in ipairs(srcfiles) do
                os.vcp(srcfile, dstfiles[idx])
            end
        end
    end
end

-- install shared libraries
function _install_shared_libraries(target, opt)
    local bindir = target:is_plat("windows", "mingw") and _get_target_bindir(target, opt) or _get_target_libdir(target, opt)

    -- get all dependent shared libraries
    local libfiles = {}
    target_utils.get_target_libfiles(target, libfiles, target:targetfile(), {}, opt)
    libfiles = table.unique(libfiles)

    -- do install
    for _, libfile in ipairs(libfiles) do
        local filename = path.filename(libfile)
        local filepath = path.join(bindir, filename)
        if os.isfile(filepath) and hash.sha256(filepath) ~= hash.sha256(libfile) then
            wprint("'%s' already exists in install dir, we are copying '%s' to overwrite it.", filepath, libfile)
        end
        _copy_file_with_symlinks(libfile, bindir)
    end
end

-- update install rpath, we can only get and update rpathdirs with `{installonly = true}`
-- e.g. add_rpathdirs("@loader_path/../lib", {installonly = true})
function _update_install_rpath(target, opt)
    if target:is_plat("windows", "mingw") then
        return
    end
    local bindir = _get_target_bindir(target, opt)
    local targetfile = path.join(bindir, target:filename())
    if target:policy("install.rpath") then
        local result, sources = target:get_from("rpathdirs", "*")
        if result and sources then
            for idx, rpathdirs in ipairs(result) do
                local source = sources[idx]
                local extraconf = target:extraconf_from("rpathdirs", source)
                for _, rpathdir in ipairs(rpathdirs) do
                    local extra = extraconf and extraconf[rpathdir] or nil
                    if extra and extra.installonly then
                        rpath_utils.insert(targetfile, rpathdir, {plat = target:plat(), arch = target:arch()})
                    end
                end
            end
        end
    end
end

-- install binary
function _install_binary(target, opt)
    if opt.libraries then
        _install_shared_libraries(target, opt)
    end
    if opt.binaries then
        local bindir = _get_target_bindir(target, opt)
        os.mkdir(bindir)
        os.vcp(target:targetfile(), bindir)
        os.trycp(target:symbolfile(), path.join(bindir, path.filename(target:symbolfile())))
        _update_install_rpath(target, opt)
    end
end

-- install shared library
function _install_shared(target, opt)
    if opt.libraries then
        local bindir = target:is_plat("windows", "mingw") and _get_target_bindir(target, opt) or _get_target_libdir(target, opt)
        os.mkdir(bindir)
        local targetfile = target:targetfile()

        if target:is_plat("windows", "mingw") then
            -- install *.lib for shared/windows (*.dll) target
            -- @see https://github.com/xmake-io/xmake/issues/714
            os.vcp(target:targetfile(), bindir)
            local libdir = _get_target_libdir(target, opt)
            local implibfile = target:artifactfile("implib")
            if os.isfile(implibfile) then
                os.mkdir(libdir)
                os.vcp(implibfile, libdir)
            end
        else
            -- install target with soname and symlink
            _copy_file_with_symlinks(targetfile, bindir)
        end
        os.trycp(target:symbolfile(), path.join(bindir, path.filename(target:symbolfile())))
        _install_shared_libraries(target, opt)
    end
    if opt.headers then
        _install_headers(target, opt)
    end
end

-- install static library
function _install_static(target, opt)
    if opt.libraries then
        local libdir = _get_target_libdir(target, opt)
        os.mkdir(libdir)
        os.vcp(target:targetfile(), libdir)
        os.trycp(target:symbolfile(), path.join(libdir, path.filename(target:symbolfile())))
    end
    if opt.headers then
        _install_headers(target, opt)
    end
end

-- install headeronly library
function _install_headeronly(target, opt)
    if opt.headers then
        _install_headers(target, opt)
    end
end

-- install moduleonly library
function _install_moduleonly(target, opt)
    if opt.headers then
        _install_headers(target, opt)
    end
end

-- install the given target
--
-- @param target    the target instance
-- @param opt       the options, e.g. {installdir = "", libdir = "", bindir = "", includedir = "",
--                                     headers = true, binaries = true, libraries = true, packages = true}
--
function main(target, opt)
    opt = opt or {}
    if opt.headers == nil then
        opt.headers = true
    end
    if opt.binaries == nil then
        opt.binaries = true
    end
    if opt.libraries == nil then
        opt.libraries = true
    end
    if opt.packages == nil then
        opt.packages = true
    end

    local installdir = opt.installdir or target:installdir()
    if not installdir then
        wprint("please use `xmake install -o installdir` or `set_installdir` to set install directory.")
        return
    end
    print("installing %s to %s ..", target:name(), installdir)

    if target:is_binary() then
        _install_binary(target, opt)
    elseif target:is_shared() then
        _install_shared(target, opt)
    elseif target:is_static() then
        _install_static(target, opt)
    elseif target:is_headeronly() then
        _install_headeronly(target, opt)
    elseif target:is_moduleonly() then
        _install_moduleonly(target, opt)
    end

    _install_files(target)
end
