--!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      OpportunityLiu
-- @file        vsxmake.lua
--

-- imports
import("core.base.option")
import("core.base.hashset")
import("vstudio.impl.vsinfo", { rootdir = path.directory(os.scriptdir()) })
import("render")
import("getinfo")
import("core.project.config")
import("core.cache.localcache")
import("vstudio.impl.vsutils", {rootdir = path.join(os.programdir(), "plugins", "project")})

local template_root = path.join(os.programdir(), "scripts", "vsxmake", "vsproj", "templates")
local template_sln = path.join(template_root, "sln", "vsxmake.sln")
local template_vcx = path.join(template_root, "vcxproj", "#target#.vcxproj")
local template_csproj = path.join(template_root, "csproj", "#target#.csproj")

local template_fil = path.join(template_root, "vcxproj.filters", "#target#.vcxproj.filters")
local template_props = path.join(template_root, "Xmake.Custom.props")
local template_targets = path.join(template_root, "Xmake.Custom.targets")
local template_items = path.join(template_root, "Xmake.Custom.items")
local template_itemfil = path.join(template_root, "Xmake.Custom.items.filters")

-- the source file extensions natively supported by the vsxmake project, keyed by the template import name
-- the `filenone` group collects everything else (e.g. files added by add_files but handled by a custom rule)
local _source_exts = {
    filec   = {".c"},
    filecxx = {".cpp", ".cc", ".cxx"},
    filempp = {".mpp", ".mxx", ".cppm", ".ixx"},
    filecu  = {".cu"},
    fileobj = {".obj", ".o"},
    filerc  = {".rc"},
    fileui  = {".ui"},
    fileqrc = {".qrc"},
    filets  = {".ts"},
    filecs  = {".cs"}
}

function _filter_files(files, includeexts, excludeexts)
    local positive = not excludeexts
    local extset = hashset.from(positive and includeexts or excludeexts)
    local f = {}
    for _, file in ipairs(files) do
        local ext = path.extension(file)
        if (positive and extset:has(ext)) or not (positive or extset:has(ext)) then
            table.insert(f, file)
        end
    end
    table.sort(f)
    return f
end

function _buildparams(info, target, default)

    local function getprop(match, opt)
        local i = info
        local r = info[match]
        if target then
            opt = table.join(target, opt)
        end
        for _, k in ipairs(opt) do
            local v = (i._targets or {})[k]
            if v == nil and i._arch_modes then
                v = i._arch_modes[k]
            end
            if v == nil and i._paths then
                v = i._paths[k]
            end
            if v == nil and i._dirs then
                v = i._dirs[k]
            end
            if v == nil and i._deps then
                v = i._deps[k]
            end
            if v == nil and i._groups then
                v = i._groups[k]
            end
            if v == nil and i._group_deps then
                v = i._group_deps[k]
            end
            if v == nil then
                v = i[k]
            end
            if v == nil then
                raise("key '" .. k .. "' not found")
            end
            i = v
            r = i[match] or r
        end

        return r or default
    end

    local function listconfig(args)
        for _, k in ipairs(args) do
            args[k] = true
        end
        local r = {}
        if args.target then
            table.insert(r, info.targets)
        end
        if args.mode then
            table.insert(r, info.modes)
        end
        if args.arch then
            table.insert(r, info.archs)
        end
        if args.group then
            table.insert(r, info.groups)
        end
        if args.group_dep then
            table.insert(r, info.group_deps)
        end
        if args.dir then
            table.insert(r, info._targets[target].dirs)
        end
        if args.dep then
            table.insert(r, info._targets[target].deps)
        end
        if args.filec then
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.filec))
        elseif args.filecxx then
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.filecxx))
        elseif args.filempp then
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.filempp))
        elseif args.filecu then
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.filecu))
        elseif args.fileobj then
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.fileobj))
        elseif args.filerc then
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.filerc))
        elseif args.fileui then -- for qt/.ui
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.fileui))
        elseif args.fileqrc then -- for qt/.qrc
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.fileqrc))
        elseif args.filets then -- for qt/.ts
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.filets))
        elseif args.filecs then -- for c#
            local files = info._targets[target].sourcefiles
            table.insert(r, _filter_files(files, _source_exts.filecs))
        elseif args.filenone then -- for custom build steps, e.g. files added by add_files but handled by a custom rule
            -- collect the source files not natively supported by vsxmake, and exclude the header/extra files
            -- to avoid duplicate display with the include group (add_extrafiles)
            local _target = info._targets[target]
            local excludeexts = {}
            for _, exts in table.orderpairs(_source_exts) do
                table.join2(excludeexts, exts)
            end
            local excludefiles = hashset.from(table.join(_target.headerfiles or {}, _target.extrafiles or {}))
            local files = {}
            for _, file in ipairs(_target.sourcefiles) do
                if not excludefiles:has(file) then
                    table.insert(files, file)
                end
            end
            table.insert(r, _filter_files(files, nil, excludeexts))
        elseif args.incc then
            local files = table.join(info._targets[target].headerfiles or {}, info._targets[target].extrafiles)
            table.insert(r, _filter_files(files, nil, {".natvis"}))
        elseif args.incnatvis then
            local files = table.join(info._targets[target].headerfiles or {}, info._targets[target].extrafiles)
            table.insert(r, _filter_files(files, {".natvis"}))
        end
        return r
    end

    return function(match, opt)
        if type(match) == "table" then
            return listconfig(match)
        end
        return getprop(match, opt)
    end
end

function _trycp(file, target, targetname)
    targetname = targetname or path.filename(file)
    local targetfile = path.join(target, targetname)
    targetfile = vsutils.translate_path(targetfile)
    if os.isfile(targetfile) then
        dprint("skipped file %s since the file already exists", path.relative(targetfile))
        return
    end
    os.cp(file, targetfile)
end

function _writefileifneeded(file, content)
    file = vsutils.translate_path(file)
    if os.isfile(file) and io.readfile(file) == content then
        dprint("skipped file %s since the file has the same content", path.relative(file))
        return
    end
    -- we need utf8 with bom encoding for unicode
    -- @see https://github.com/xmake-io/xmake/issues/1689
    io.writefile(file, content, {encoding = "utf8bom"})
end

-- save plugin arguments for `plugin.vsxmake.autoupdate`
-- @see https://github.com/xmake-io/xmake/issues/1895
function _save_plugin_arguments()
    local vsxmake_cache = localcache.cache("vsxmake")
    for _, name in ipairs({"kind", "modes", "archs", "outputdir"}) do
        vsxmake_cache:set(name, option.get(name))
    end
    vsxmake_cache:save()
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

-- make
function make(version)

    if not version then
        version = tonumber(config.get("vs"))
        if not version then
            return function(outputdir)
                raise("invalid vs version, run `xmake f --vs=20xx`")
            end
        end
    end

    return function(outputdir)
        vprint("using project kind vs%d", version)
        assert(version >= 2010, "vsxmake does not support vs version lower than 2010")

        -- get info and params
        local info = getinfo(outputdir, vsinfo(version))
        local paramsprovidersln = _buildparams(info)

        -- write solution file
        local sln = path.join(info.solution_dir, info.slnfile .. ".sln")
        _writefileifneeded(sln, render(template_sln, "#([A-Za-z0-9_,%.%*%(%)]+)#", "@([^@]+)@", paramsprovidersln))

        -- add solution custom file
        _trycp(template_props, info.vcxproj_rootdir)
        _trycp(template_targets, info.vcxproj_rootdir)

        for _, target in ipairs(info.targets) do
            local paramsprovidertarget = _buildparams(info, target, "<!-- nil -->")
            local _target = info._targets[target]
            local vcxproj_dir = _target.vcxprojdir
            local projkind = _target.projkind or "cxx"
            local projext = _target.projext or "vcxproj"

            -- write project file
            local proj_file = path.join(vcxproj_dir, target .. "." .. projext)
            local proj_template = (projkind == "csharp") and template_csproj or template_vcx
            _writefileifneeded(proj_file, render(proj_template, "#([A-Za-z0-9_,%.%*%(%)]+)#", "@([^@]+)@", paramsprovidertarget))

            -- .csproj does not use a .filters sidecar (legacy .vcxproj-only mechanism)
            if projkind ~= "csharp" then
                local vcxproj_filters = path.join(vcxproj_dir, target .. ".vcxproj.filters")
                _writefileifneeded(vcxproj_filters, render(template_fil, "#([A-Za-z0-9_,%.%*%(%)]+)#", "@([^@]+)@", paramsprovidertarget))
            end

            -- add project custom file
            _trycp(template_props, vcxproj_dir)
            _trycp(template_targets, vcxproj_dir)
            _trycp(template_items, vcxproj_dir)
            _trycp(template_itemfil, vcxproj_dir)
        end

        -- clear config and local cache
        _clear_cache()

        -- save plugin arguments for autoupdate
        _save_plugin_arguments()
    end
end
