--!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        vs201x.lua
--

-- imports
import("core.base.option")
import("core.base.colors")
import("core.base.hashset")
import("core.project.rule")
import("core.project.config")
import("core.project.project")
import("core.platform.platform")
import("core.tool.compiler")
import("core.tool.linker")
import("core.tool.toolchain")
import("vs201x_solution")
import("vs201x_vcxproj")
import("vs201x_vcxproj_filters")
import("vsutils")
import("core.cache.memcache")
import("core.cache.localcache")
import("private.action.require.install", {alias = "install_requires"})
import("private.action.run.runenvs")
import("actions.config.configfiles", {alias = "generate_configfiles", rootdir = os.programdir()})
import("private.utils.batchcmds")
import("plugins.project.utils.target_cmds", {rootdir = os.programdir()})
import("private.utils.target", {alias = "target_utils"})

function _translate_path(dir, vcxprojdir)
    if dir == nil then
        return ""
    end
    if type(dir) == "string" then
        dir = path.translate(dir)
        if dir == "" then
            return ""
        end
        if path.is_absolute(dir) then
            if dir:startswith(project.directory()) then
                return vsutils.escape(path.relative(dir, vcxprojdir))
            end
            return vsutils.escape(dir)
        else
            return vsutils.escape(path.relative(path.absolute(dir), vcxprojdir))
        end
    end
    local r = {}
    for k, v in ipairs(dir) do
        r[k] = _translate_path(v, vcxprojdir)
    end
    r = table.unique(r)
    return path.joinenv(r)
end

-- clear cache
function _clear_cache()
    localcache.clear("detect")
    localcache.clear("option")
    localcache.clear("package")
    localcache.clear("toolchain")

    -- force recheck
    localcache.set("config", "recheck", true)

    localcache.save()
end

-- get c++ modules rules
function _get_cxxmodules_rules()
    return {"c++.build.modules", "c++.build.modules.builder"}
end

-- get command string
function _get_command_string(cmd, vcxprojdir)
    local kind = cmd.kind
    local opt = cmd.opt
    if cmd.program then
        local argv = {}
        for _, v in ipairs(table.join(cmd.program, cmd.argv)) do
            if path.instance_of(v) then
                v = v:clone():set(_translate_path(v:rawstr(), vcxprojdir)):str()
            elseif path.is_absolute(v) then
                v = _translate_path(v, vcxprojdir)
            end
            table.insert(argv, v)
        end
        local command = os.args(argv)
        if opt and opt.curdir then
            command = string.format("pushd \"%s\"\n%s\npopd", _translate_path(opt.curdir, vcxprojdir), command)
        end
        return command
    elseif kind == "cp" then
        return string.format("copy /Y \"%s\" \"%s\"", _translate_path(cmd.srcpath, vcxprojdir), _translate_path(cmd.dstpath, vcxprojdir))
    elseif kind == "rm" then
        return string.format("del /F /Q \"%s\" || rmdir /S /Q \"%s\"", _translate_path(cmd.filepath, vcxprojdir), _translate_path(cmd.filepath, vcxprojdir))
    elseif kind == "rmdir" then
        return string.format("rmdir /S /Q \"%s\"", _translate_path(cmd.filepath, vcxprojdir))
    elseif kind == "mv" then
        return string.format("rename \"%s\" \"%s\"", _translate_path(cmd.srcpath, vcxprojdir), _translate_path(cmd.dstpath, vcxprojdir))
    elseif kind == "cd" then
        return string.format("cd \"%s\"", _translate_path(cmd.dir, vcxprojdir))
    elseif kind == "mkdir" then
        local dir = _translate_path(cmd.dir, vcxprojdir)
        return string.format("if not exist \"%s\" mkdir \"%s\"", dir, dir)
    elseif kind == "show" or kind == "show_progress" then
        local text = string.format(cmd.format, table.unpack(cmd.argv))
        return string.format("echo %s", colors.ignore(text))
    end
end

-- make custom commands
function _make_custom_commands(target, vcxprojdir)
    -- https://github.com/xmake-io/xmake/issues/2337
    target:data_set("plugin.project.kind", "vs")
    -- https://github.com/xmake-io/xmake/issues/2258
    target:data_set("plugin.project.translate_path", function (p)
        return _translate_path(p, vcxprojdir)
    end)

    -- ignore c++ modules rules
    local ignored_rules = _get_cxxmodules_rules()

    -- add before commands
    -- we use irpairs(groups), because the last group that should be given the highest priority.
    -- rule.on_buildcmd_files should also be executed before building the target
    local cmds_before = target_cmds.get_target_buildcmds(target, {ignored_rules = ignored_rules, stages = {"before", "on"}})

    -- add after commands
    local cmds_after = target_cmds.get_target_buildcmds(target, {ignored_rules = ignored_rules, stages = {"after"}})

    local commands = {}
    for _, cmd in ipairs(cmds_before) do
        commands.before = commands.before or {}
        table.insert(commands.before, _get_command_string(cmd, vcxprojdir))
    end
    for _, cmd in ipairs(cmds_after) do
        commands.after = commands.after or {}
        table.insert(commands.after, _get_command_string(cmd, vcxprojdir))
    end
    return commands
end

-- make target info
function _make_targetinfo(mode, arch, target, vcxprojdir)

    -- init target info
    local targetinfo = { mode = mode, arch = vsutils.vsarch(arch) }

    -- get sdk version
    local msvc = toolchain.load("msvc")
    if msvc then
        local vcvars = msvc:config("vcvars")
        if vcvars then
            targetinfo.sdkver = vcvars.WindowsSDKVersion
        end
    end

    -- save c/c++ precompiled output file (.pch)
    targetinfo.pcoutputfile = target:pcoutputfile("c")
    targetinfo.pcxxoutputfile = target:pcoutputfile("cxx")
    target:set("pcheader", nil)
    target:set("pcxxheader", nil)

    -- save languages
    targetinfo.languages = table.wrap(target:get("languages"))

    -- save symbols
    targetinfo.symbols = target:get("symbols")

    -- has modules
    targetinfo.has_modules = target:data("cxx.has_modules")

    -- save target kind
    targetinfo.targetkind = target:kind()
    if target:is_phony() or target:is_headeronly() then
        return targetinfo
    end

    -- save target file
    targetinfo.targetfile = target:targetfile()

    -- save symbol file
    targetinfo.symbolfile = target:symbolfile()

    -- save sourcekinds
    targetinfo.sourcekinds = target:sourcekinds()

    -- save target dir
    targetinfo.targetdir = target:targetdir()

    -- save object dir
    targetinfo.objectdir = target:objectdir()

    -- save compiler flags and cmds
    local firstcompflags = nil
    targetinfo.compflags = {}
    targetinfo.compargvs = {}
    local sourcebatches = target:sourcebatches()
    for _, sourcebatch in table.orderpairs(sourcebatches) do
        local sourcekind = sourcebatch.sourcekind
        local rulename = sourcebatch.rulename
        if sourcekind then
            for _, sourcefile in ipairs(sourcebatch.sourcefiles) do
                local compflags = compiler.compflags(sourcefile, {target = target, sourcekind = sourcekind})
                if not firstcompflags and (rulename == "c.build" or rulename == "c++.build" or rulename == "c++.build.modules" or rulename == "cuda.build") then
                    firstcompflags = compflags
                end
                targetinfo.compflags[sourcefile] = compflags
                targetinfo.compargvs[sourcefile] = table.join(compiler.compargv("__sourcefile__", "__objectfile__", {sourcekind = sourcekind, target = target}))

                -- detect manifest
                -- @see https://github.com/xmake-io/xmake/issues/2176
                if sourcekind == "mrc" and os.isfile(sourcefile) then
                    local resoucedata = io.readfile(sourcefile)
                    if resoucedata and resoucedata:find("RT_MANIFEST") then
                        targetinfo.manifest_embed = false
                    end
                end
            end
        end
    end

    -- save sourcebatches
    targetinfo.sourcebatches = target:sourcebatches()

    -- save linker flags
    local linkflags = linker.linkflags(target:is_moduleonly() and 'static' or target:kind(), target:sourcekinds(), {target = target})
    targetinfo.linkflags = linkflags

    if table.contains(target:sourcekinds(), "cu") then
        -- save cuda linker flags
        local linkinst = linker.load("gpucode", "cu", {target = target})
        targetinfo.culinkflags = linkinst:linkflags({target = target})

        -- save cuda devlink status
        targetinfo.cudevlink = target:policy("build.cuda.devlink") or target:values("cuda.build.devlink")
    end

    -- save execution dir (when executed from VS)
    targetinfo.rundir = target:is_moduleonly() and "" or target:rundir()

    -- save runenvs
    local targetrunenvs = {}
    local addrunenvs, setrunenvs = runenvs.make(target)
    for k, v in table.orderpairs(target:pkgenvs()) do
        addrunenvs = addrunenvs or {}
        addrunenvs[k] = table.join(table.wrap(addrunenvs[k]), path.splitenv(v))
    end
    for _, dep in ipairs(target:orderdeps()) do
        for k, v in table.orderpairs(dep:pkgenvs()) do
            addrunenvs = addrunenvs or {}
            addrunenvs[k] = table.join(table.wrap(addrunenvs[k]), path.splitenv(v))
        end
    end
    for k, v in table.orderpairs(addrunenvs) do
        if k:upper() == "PATH" then
            targetrunenvs[k] = _translate_path(v, vcxprojdir) .. ";$([System.Environment]::GetEnvironmentVariable('" .. k .. "'))"
        else
            targetrunenvs[k] = path.joinenv(v) .. ";$([System.Environment]::GetEnvironmentVariable('" .. k .."'))"
        end
    end
    for k, v in table.orderpairs(setrunenvs) do
        if #v == 1 then
            v = v[1]
            if path.is_absolute(v) and v:startswith(project.directory()) then
                targetrunenvs[k] = _translate_path(v, vcxprojdir)
            else
                targetrunenvs[k] = v[1]
            end
        else
            targetrunenvs[k] = path.joinenv(v)
        end
    end
    local runenvstr = {}
    for k, v in table.orderpairs(targetrunenvs) do
        table.insert(runenvstr, k .. "=" .. v)
    end
    targetinfo.runenvs = table.concat(runenvstr, "\n")

    -- use mfc? save the mfc runtime kind
    if target:rule("win.sdk.mfc.shared_app") or target:rule("win.sdk.mfc.shared") then
        targetinfo.usemfc = "Dynamic"
    elseif target:rule("win.sdk.mfc.static_app") or target:rule("win.sdk.mfc.static") then
        targetinfo.usemfc = "Static"
    end

    -- set unicode
    for _, flag in ipairs(firstcompflags) do
        if flag:find("[%-|/]DUNICODE") then
            targetinfo.unicode = true
            break
        end
    end

    -- save custom commands
    targetinfo.commands = _make_custom_commands(target, vcxprojdir)
    return targetinfo
end

function _make_vsinfo_modes()
    local vsinfo_modes = {}
    local modes = option.get("modes")
    if modes then
        if not modes:find("\"") then
            modes = modes:gsub(",", path.envsep())
        end
        for _, mode in ipairs(path.splitenv(modes)) do
            table.insert(vsinfo_modes, mode:trim())
        end
    else
        vsinfo_modes = project.modes()
    end
    if not vsinfo_modes or #vsinfo_modes == 0 then
        vsinfo_modes = { config.mode() }
    end
    return vsinfo_modes
end

function _make_vsinfo_archs()
    local vsinfo_archs = {}
    local archs = option.get("archs")
    if archs then
        if not archs:find("\"") then
            archs = archs:gsub(",", path.envsep())
        end
        for _, arch in ipairs(path.splitenv(archs)) do
            table.insert(vsinfo_archs, arch:trim())
        end
    else
        -- we use it first if global set_arch("xx") is setted in xmake.lua
        vsinfo_archs = project.get("target.arch")
        if not vsinfo_archs then
            -- for set_allowedarchs()
            local allowed_archs = project.allowed_archs(config.plat())
            if allowed_archs then
                vsinfo_archs = allowed_archs:to_array()
            end
        end
        if not vsinfo_archs then
            local default_archs = toolchain.load("msvc"):config("vcarchs")
            if not default_archs then
                default_archs = platform.archs()
            end
            if default_archs then
                default_archs = hashset.from(table.wrap(default_archs))
                -- just generate single arch by default to avoid some fails for installing packages.
                -- @see https://github.com/xmake-io/xmake/issues/3268
                local arch = config.arch()
                if default_archs:has(arch) then
                    vsinfo_archs = { arch }
                else
                    default_archs:remove("arm64")
                    vsinfo_archs = default_archs:to_array()
                end
            end
        end
    end
    if not vsinfo_archs or #vsinfo_archs == 0 then
        vsinfo_archs = { config.arch() }
    end
    return vsinfo_archs
end

-- make vstudio project
function make(outputdir, vsinfo)
    local oldir = os.cd(project.directory())

    -- prepare targets
    target_cmds.prepare_targets()

    -- init vsinfo
    vsinfo.solution_dir = path.join(outputdir, "vs" .. vsinfo.vstudio_version)
    vsinfo.modes = _make_vsinfo_modes()
    vsinfo.archs = _make_vsinfo_archs()

    -- load targets
    local targets = {}
    for mode_idx, mode in ipairs(vsinfo.modes) do
        for arch_idx, arch in ipairs(vsinfo.archs) do

            -- trace
            print("checking for %s.%s ...", mode, arch)

            -- reload config, project and platform
            if mode ~= config.mode() or arch ~= config.arch() then

                -- reset project configs and caches
                vsutils.reset_config_and_caches(mode, arch)

                -- check platform
                platform.load(config.plat(), arch):check()

                -- check project options
                project.check_options()

                -- install and update requires
                install_requires()

                -- check target toolchains
                target_utils.check_target_toolchains()

                -- load targets
                project.load_targets()

                -- update config files
                generate_configfiles()
            end

            -- ensure to enter project directory
            os.cd(project.directory())

            -- save targets
            local project_targets = target_utils.get_project_targets()
            for targetname, target in table.orderpairs(project_targets) do

                -- make target with the given mode and arch
                targets[targetname] = targets[targetname] or {}
                local _target = targets[targetname]

                -- the vcxproj directory
                _target.project_dir = path.join(vsinfo.solution_dir, targetname)

                -- save c/c++ precompiled header
                _target.pcheader   = target:pcheaderfile("c")     -- header.h
                _target.pcxxheader = target:pcheaderfile("cxx")   -- header.[hpp|inl]

                -- init target info
                _target.name = targetname
                _target.kind = target:kind()
                _target.scriptdir = target:scriptdir()
                _target.info = _target.info or {}
                table.insert(_target.info, _make_targetinfo(mode, arch, target, _target.project_dir))

                -- save all sourcefiles and headerfiles
                _target.sourcefiles = table.unique(table.join(_target.sourcefiles or {}, (target:sourcefiles())))
                _target.headerfiles = table.unique(table.join(_target.headerfiles or {}, (target:headerfiles())))
                _target.extrafiles = table.unique(table.join(_target.extrafiles or {}, (target:extrafiles())))

                -- sort them to stabilize generation
                table.sort(_target.sourcefiles)
                table.sort(_target.headerfiles)
                table.sort(_target.extrafiles)

                -- save file groups
                _target.filegroups = table.unique(table.join(_target.filegroups or {}, target:get("filegroups")))

                -- save references to deps
                for _, dep in ipairs(target:orderdeps()) do
                    _target.deps = _target.deps or {}
                    local dep_name = dep:name()
                    _target.deps[dep_name] = path.relative(path.join(vsinfo.solution_dir, dep_name, dep_name .. ".vcxproj"), _target.project_dir)
                end

                for filegroup, groupconf in pairs(target:extraconf("filegroups")) do
                    _target.filegroups_extraconf = _target.filegroups_extraconf or {}
                    local mergedconf = _target.filegroups_extraconf[filegroup]
                    if not mergedconf then
                        mergedconf = {}
                        _target.filegroups_extraconf[filegroup] = mergedconf
                    end

                    if groupconf.rootdir then
                        mergedconf.rootdir = table.unique(table.join(mergedconf.rootdir or {}, table.wrap(groupconf.rootdir)))
                    end
                    if groupconf.files then
                        mergedconf.files = table.unique(table.join(mergedconf.files or {}, table.wrap(groupconf.files)))
                    end
                    mergedconf.plain = groupconf.plain or mergedconf.plain
                end
            end
        end
    end

    -- make solution
    vs201x_solution.make(vsinfo)

    -- make .vcxproj
    for _, target in table.orderpairs(targets) do
        vs201x_vcxproj.make(vsinfo, target)
        vs201x_vcxproj_filters.make(vsinfo, target)
    end

    -- clear local cache
    _clear_cache()
    os.cd(oldir)
end
