-- Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
-- For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
--- helper scripts
-- @module helpers
local lfs = require('lfs')
local json = require('lunajson')

local helpers = {}

local function dirtree(dir)
    assert(dir and dir ~= "", "directory parameter is missing or empty")
    if string.sub(dir, -1) == "/" then
        dir = string.sub(dir, 1, -2)
    end

    local function yieldtree(dir)
        for entry in lfs.dir(dir) do
            if entry ~= "." and entry ~= ".." then
                entry = dir .. "/" .. entry
                local attr = lfs.attributes(entry)
                coroutine.yield(entry, attr)
                if attr.mode == "directory" then
                    yieldtree(entry)
                end
            end
        end
    end

    return coroutine.wrap(function()
        yieldtree(dir)
    end)
end

local function ends_with(str, ending)
    return ending == "" or str:sub(-#ending) == ending
end

local function starts_with(str, start)
    return str:sub(1, #start) == start
end

local function build_path(prefix, name)
    if not ends_with(prefix, '/') then
        prefix = prefix .. '/'
    end
    if starts_with(name, '/') then
        name = name:sub(2, -1)
    end
    return prefix .. name
end

local function strip_from_prefix(prefix, path)
    local name = path:gsub(prefix, "")
    name = name:sub(2, -1)
    return name
end

local function basedir(p)
    return p:gsub('[^\\/]+[\\/]?$', '')
end

--- Copy file
-- @function copy_file
-- @param filename_in input file
-- @param filename_out output file
function helpers.copy_file(filename_in, filename_out)
    local size = 1024 * 512
    local size_in = lfs.attributes(filename_in, "size")
    local fd_in = assert(io.open(filename_in, "r"))
    local fd_out = assert(io.open(filename_out, "w"))
    collectgarbage()
    while true do
        local block = fd_in:read(size)
        if not block then
            break
        end
        assert(fd_out:write(block))
    end
    fd_in:close()
    fd_out:close()
end

--- Remove directory and its contents
-- @function rmdir
-- @param dir directory to remove
function helpers.rmdir(dir)
    for file in lfs.dir(dir) do
        local file_path = dir .. "/" .. file
        if file ~= "." and file ~= ".." then
            if lfs.attributes(file_path, "mode") == "file" then
                assert(os.remove(file_path))
            elseif lfs.attributes(file_path, "mode") == "directory" then
                helpers.rmdir(file_path)
            end
        end
    end
    lfs.rmdir(dir)
end

--- Remove directory content
-- @function rmdir_content
-- @param dir directory path
function helpers.rmdir_content(dir)
    for file in lfs.dir(dir) do
        local file_path = dir .. "/" .. file
        if file ~= "." and file ~= ".." then
            if lfs.attributes(file_path, "mode") == "file" then
                assert(os.remove(file_path))
            elseif lfs.attributes(file_path, "mode") == "directory" then
                helpers.rmdir(file_path)
            end
        end
    end
end

--- Remove all files from directory without touching internal directories
-- @function rm_files_from_dir
-- @param dir directory to remove files from
function helpers.rm_files_from_dir(dir)
    for file in lfs.dir(dir) do
        local file_path = dir .. "/" .. file
        if file ~= "." and file ~= ".." then
            if lfs.attributes(file_path, "mode") == "file" then
                assert(os.remove(file_path))
            end
        end
    end
end

--- Copy directory recursively
-- @function copy_dir
-- @param from source directory
-- @param where target directory
function helpers.copy_dir(from, where)
    for filename, attr in dirtree(from) do
        local name = filename:gsub(from, "")
        if attr.mode == "directory" then
            assert(lfs.mkdir(build_path(where, name)))
        else
            helpers.copy_file(filename, build_path(where, name))
        end
    end
end

--- Move directory recursively
-- @function move_dir
-- @param from source directory
-- @param where target directory
function helpers.move_dir(from, where)
    for filename, attr in dirtree(from) do
        local name = filename:gsub(from, "")
        if attr.mode == "directory" then
            assert(lfs.mkdir(build_path(where, name)))
        else
            assert(os.rename(build_path(from, name), build_path(where, name)))
        end
    end
end

--- Copy directory recursively using regex filter
-- @function copy_dir_filtered
-- @param from source directory
-- @param where target directory
-- @param matcher regex expression
function helpers.copy_dir_filtered(from, where, filter)
    for filename, attr in dirtree(from) do
        local name = strip_from_prefix(from, filename)
        if name:match(filter) then
            if attr.mode == "directory" then
                assert(lfs.mkdir(build_path(where, name)))
            else
                helpers.copy_file(filename, build_path(where, name))
            end
        end
    end
end

--- Get the size of specified directory using regex filter
-- @function dir_size_filtered
-- @param path directory path
-- @param matcher regex expression
-- @return total size of directory in bytes
function helpers.dir_size_filtered(path, filter)
    local total_size = 0
    for filename, attr in dirtree(path) do
        local name = strip_from_prefix(path, filename)
        if name:match(filter) then
            total_size = total_size + attr.size
        end
    end
    return total_size
end

--- Get the size of specified directory
-- @function dir_size
-- @param path directory path
-- @return total size of directory in bytes
function helpers.dir_size(path)
    return helpers.dir_size_regex('.*')
end

--- Check if specified dir/file exists
-- @function exists
-- @param path directory or file path
-- @return true or false
function helpers.exists(path)
    local ret = lfs.attributes(path)
    return ret ~= nil
end

--- Read the whole file at once 
-- @function read_whole_file
-- @param file file path
-- @return file contents
function helpers.read_whole_file(file)
    local f = assert(io.open(file, "rb"))
    local content = assert(f:read("*a"))
    f:close()
    return content
end

--- Extract filename from filename path
-- @function get_filename
-- @param file file path
-- @return filename
function helpers.get_filename(file)
    return file:match("^.+/(.+)$")
end

--- Extract file extension from filename
-- @function get_file_extension
-- @param file file path
-- @return file extension
function helpers.get_file_extension(file)
    return file:match("^.+(%..+)$")
end

--- Strips file name from its extension
-- @function strip_from_extension
-- @param file file path
-- @return file name without extension
function helpers.strip_from_extension(file)
    return file:match("^(.*)%..*$")
end

--- Create directory and all required subdirectories
-- @function mkdirp
-- @param file file path
function helpers.mkdirp(p)
    if lfs.attributes(p, 'mode') == 'directory' then
        return nil, 'already exists'
    end

    local b = basedir(p)
    if #b > 0 and lfs.attributes(b, 'mode') ~= 'directory' then
        local r, m = helpers.mkdirp(b)
        if not r then
            return r, m
        end
    end
    return lfs.mkdir(p)
end

--- Get OS version from the 'version.json'
-- @function get_os_version
-- @param file file path
function helpers.get_os_version(file)
    local contents = helpers.read_whole_file(file)
    local root = json.decode(contents)
    return root.os.version
end

return helpers
