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

-- imports
import("core.base.object")

-- define module
local filesync = filesync or object()

-- init filesync
function filesync:init(rootdir, manifest_file)
    self._ROOTDIR = rootdir
    self._MANIFEST_FILE = manifest_file
end

-- get root directory
function filesync:rootdir()
    return self._ROOTDIR
end

-- get ignore files
function filesync:ignorefiles()
    local ignorefiles = self._IGNOREFILES
    if not ignorefiles then
        ignorefiles = {}
        self:_ignorefiles_load(ignorefiles)
        self._IGNOREFILES = ignorefiles
    end
    return ignorefiles
end

-- add ignore files
function filesync:ignorefiles_add(...)
    table.join2(self:ignorefiles(), ...)
end

-- get manifest
function filesync:manifest()
    local manifest = self._MANIFEST
    if not manifest then
        local manifest_file = self:manifest_file()
        if manifest_file and os.isfile(manifest_file) then
            manifest = io.load(manifest_file)
        end
        manifest = manifest or {}
        self._MANIFEST = manifest
    end
    return manifest
end

-- save manifest file
function filesync:manifest_save()
    local manifest_file = self:manifest_file()
    if manifest_file then
        io.save(manifest_file, self:manifest())
    end
end

-- get manifest file
function filesync:manifest_file()
    return self._MANIFEST_FILE
end

-- do snapshot, it will re-scan all and update to manifest file
function filesync:snapshot()
    local rootdir = self:rootdir()
    assert(rootdir and os.isdir(rootdir), "get snapshot %s failed, rootdir not found!", rootdir)
    local manifest = {}
    local manifest_old = self:manifest()
    local ignorefiles = self:ignorefiles()
    if ignorefiles then
        ignorefiles = "|" .. table.concat(ignorefiles, "|")
    end
    local count = 0
    -- we should not translate the whole pattern with ignorefiles in path.join,
    -- because the joined pattern string may be very long (> TB_PATH_MAXN)
    -- https://github.com/xmake-io/xmake/issues/6962
    for _, filepath in ipairs(os.files(path.join(rootdir, "**") .. ignorefiles)) do
        local fileitem = path.relative(filepath, rootdir)
        if fileitem then
            -- we should always use '/' in path key for supporting linux & windows
            -- https://github.com/xmake-io/xmake/issues/2488
            if is_host("windows") then
                fileitem = fileitem:gsub("\\", "/")
            end
            local manifest_info = manifest_old[fileitem]
            local mtime = os.mtime(filepath)
            if not manifest_info or not manifest_info.mtime or mtime > manifest_info.mtime then
                manifest[fileitem] = {sha256 = hash.sha256(filepath), mtime = mtime}
            else
                manifest[fileitem] = manifest_info
            end
            count = count + 1
        end
    end
    self._MANIFEST = manifest
    self:manifest_save()
    return manifest, count
end

-- update file
function filesync:update(fileitem, filepath, sha256)
    local mtime = os.mtime(filepath)
    local manifest = self:manifest()
    sha256 = sha256 or hash.sha256(filepath)
    manifest[fileitem] = {sha256 = sha256, mtime = mtime}
end

-- remove file
function filesync:remove(fileitem)
    local manifest = self:manifest()
    manifest[fileitem] = nil
end

-- load ignore files from .gitignore files
function filesync:_ignorefiles_load(ignorefiles)
    local rootdir = self:rootdir()
    local gitignore_files = os.files(path.join(rootdir, "**", ".gitignore"))
    if os.isfile(path.join(rootdir, ".gitignore")) then
        table.insert(gitignore_files, path.join(rootdir, ".gitignore"))
    end
    for _, gitignore_file in ipairs(gitignore_files) do
        local gitroot = path.directory(gitignore_file)
        local gitignore = io.open(gitignore_file, "r")
        for line in gitignore:lines() do
            local line = line:trim()
            if #line > 0 and not line:startswith("#") then
                local filepath = path.join(gitroot, line)
                local pattern = path.relative(filepath, rootdir)
                if pattern then
                    if line:endswith(path.sep()) or os.isdir(line) then
                        table.insert(ignorefiles, path.join(pattern, "**"))
                    elseif os.isfile(line) then
                        table.insert(ignorefiles, pattern)
                    elseif line:find("*.", 1, true) then
                        table.insert(ignorefiles, (pattern:gsub("%*%.", "**.")))
                    else
                        table.insert(ignorefiles, pattern)
                        table.insert(ignorefiles, path.join(pattern, "**"))
                    end
                end
            end
        end
        gitignore:close()
    end
end

function main(rootdir, manifest_file)
    local instance = filesync()
    instance:init(rootdir, manifest_file)
    return instance
end
