-- Lua has no parallelizm, also _set_data does not call any lua functions so
-- may be reused one global ffi buffer per lua_State
local canvas_ffi_buffer
local canvas_ffi_buffer_size = 0

local ipairs_mt_supported = false
for i, _ in ipairs(setmetatable({l={1}}, {
    __ipairs=function(self) return ipairs(self.l) end})) do
    ipairs_mt_supported = true
end

if not ipairs_mt_supported then
    local raw_ipairs = ipairs
    ipairs = function(t)
        local metatable = getmetatable(t)
        if metatable and metatable.__ipairs then
            return metatable.__ipairs(t)
        end
        return raw_ipairs(t)
    end
end

function await(co)
    local res, err
    while coroutine.status(co) ~= 'dead' do
        coroutine.yield()
        res, err = coroutine.resume(co)
        if err then
            return res, err
        end
    end
    return res, err
end

local _ffi = ffi
function __vc_Canvas_set_data(self, data)
    if type(data) == "cdata" then
        self:_set_data(tostring(_ffi.cast("uintptr_t", data)))
    end
    local width = self.width
    local height = self.height

    local size = width * height * 4
    if size > canvas_ffi_buffer_size then
        canvas_ffi_buffer = _ffi.new(
            string.format("unsigned char[%s]", size)
        )
        canvas_ffi_buffer_size = size
    end
    for i=0, size - 1 do
        canvas_ffi_buffer[i] = data[i + 1]
    end
    self:_set_data(tostring(_ffi.cast("uintptr_t", canvas_ffi_buffer)))
end

function crc32(bytes, chksum)
    local chksum = chksum or 0

    local length = #bytes
    if type(bytes) == "table" then
        local buffer_len = _ffi.new('int[1]', length)
        local buffer = _ffi.new(
            string.format("char[%s]", length)
        )
        for i=1, length do
            buffer[i - 1] = bytes[i]
        end
        bytes = _ffi.string(buffer, buffer_len[0])
    end
    return _crc32(bytes, chksum)
end

-- Check if given table is an array
function is_array(x)
    if #x > 0 then
        return true
    end
    for k, v in pairs(x) do
        return false
    end
    return true
end

-- Get entry-point and filename from `entry-point:filename` path 
function parse_path(path)
    local index = string.find(path, ':')
    if index == nil then
        error("invalid path syntax (':' missing)")
    end
    return string.sub(path, 1, index-1), string.sub(path, index+1, -1)
end

function pack.is_installed(packid)
    return file.isfile(packid..":package.json")
end

function pack.data_file(packid, name)
    file.mkdirs("world:data/"..packid)
    return "world:data/"..packid.."/"..name
end

function pack.shared_file(packid, name)
    file.mkdirs("config:"..packid)
    return "config:"..packid.."/"..name
end


function timeit(iters, func, ...)
    local tm = os.clock()
    for i=1,iters do
        func(...)
    end
    print("[time mcs]", (os.clock()-tm) * 1000000)
end

----------------------------------------------

function math.clamp(_in, low, high)
    return math.min(math.max(_in, low), high)
end

function math.rand(low, high)
    return low + (high - low) * math.random()
end

function math.normalize(num, conf)
    conf = conf or 1

    return (num / conf) % 1
end

function math.round(num, places)
    places = places or 0

    local mult = 10 ^ places
    return math.floor(num * mult + 0.5) / mult
end

function math.sum(...)
    local numbers = nil
    local sum = 0

    if type(...) == "table" then
        numbers = ...
    else
        numbers = {...}
    end

    for _, v in ipairs(numbers) do
        sum = sum + v
    end

    return sum
end

----------------------------------------------

function table.copy(t)
    local copied = {}

    for k, v in pairs(t) do
        copied[k] = v
    end

    return copied
end

function table.deep_copy(t)
    local copied = {}

    for k, v in pairs(t) do
        if type(v) == "table" then
            copied[k] = table.deep_copy(v)
        else
            copied[k] = v
        end
    end

    return setmetatable(copied, getmetatable(t))
end

function table.count_pairs(t)
    local count = 0

    for k, v in pairs(t) do
        count = count + 1
    end

    return count
end

function table.random(t)
    return t[math.random(1, #t)]
end

function table.shuffle(t)
    for i = #t, 2, -1 do
        local j = math.random(i)
        t[i], t[j] = t[j], t[i]
    end

    return t
end

function table.merge(t1, t2)
    for i, v in pairs(t2) do
        if type(i) == "number" then
            t1[#t1 + 1] = v
        elseif t1[i] == nil then
            t1[i] = v
        end
    end

    return t1
end

function table.map(t, func)
    for i, v in pairs(t) do
        t[i] = func(i, v)
    end

    return t
end

function table.filter(t, func)

    for i = #t, 1, -1 do
        if not func(i, t[i]) then
            table.remove(t, i)
        end
    end

    local size = #t

    for i, v in pairs(t) do
        local i_type = type(i)
        if i_type == "number" then
            if i < 1 or i > size then
                if not func(i, v) then
                    t[i] = nil
                end
            end
        else
            if not func(i, v) then
                t[i] = nil
            end
        end
    end

    return t
end

function table.set_default(t, key, default)
    if t[key] == nil then
        t[key] = default
        return default
    end

    return t[key]
end

function table.flat(t)
    local flat = {}

    for _, v in pairs(t) do
        if type(v) == "table" then
            table.merge(flat, v)
        else
            table.insert(flat, v)
        end
    end

    return flat
end

function table.deep_flat(t)
    local flat = {}

    for _, v in pairs(t) do
        if type(v) == "table" then
            table.merge(flat, table.deep_flat(v))
        else
            table.insert(flat, v)
        end
    end

    return flat
end

function table.sub(arr, start, stop)
    local res = {}
    start = start or 1
    stop = stop or #arr

    for i = start, stop do
        table.insert(res, arr[i])
    end

    return res
end

----------------------------------------------

local pattern_escape_replacements = {
    ["("] = "%(",
    [")"] = "%)",
    ["."] = "%.",
    ["%"] = "%%",
    ["+"] = "%+",
    ["-"] = "%-",
    ["*"] = "%*",
    ["?"] = "%?",
    ["["] = "%[",
    ["]"] = "%]",
    ["^"] = "%^",
    ["$"] = "%$",
    ["\0"] = "%z"
}

function string.pattern_safe(str)
    return string.gsub(str, ".", pattern_escape_replacements)
end

local string_sub = string.sub
local string_find = string.find
local string_len = string.len
function string.explode(separator, str, withpattern)
    if (withpattern == nil) then withpattern = false end

    local ret = {}
    local current_pos = 1

    for i = 1, string_len(str) do
        local start_pos, end_pos = string_find(
            str, separator, current_pos, not withpattern)
        if (not start_pos) then break end
        ret[i] = string_sub(str, current_pos, start_pos - 1)
        current_pos = end_pos + 1
    end

    ret[#ret + 1] = string_sub(str, current_pos)

    return ret
end

function string.split(str, delimiter)
    return string.explode(delimiter, str)
end

function string.formatted_time(seconds, format)
    if (not seconds) then seconds = 0 end
    local hours = math.floor(seconds / 3600)
    local minutes = math.floor((seconds / 60) % 60)
    local millisecs = (seconds - math.floor(seconds)) * 1000
    seconds = math.floor(seconds % 60)

    if (format) then
        return string.format(format, minutes, seconds, millisecs)
    else
        return { h = hours, m = minutes, s = seconds, ms = millisecs }
    end
end

function string.replace(str, tofind, toreplace)
    local tbl = string.explode(tofind, str)
    if (tbl[1]) then return table.concat(tbl, toreplace) end
    return str
end

function string.trim(s, char)
    if char then char = string.pattern_safe(char) else char = "%s" end
    return string.match(s, "^" .. char .. "*(.-)" .. char .. "*$") or s
end

function string.trim_right(s, char)
    if char then char = string.pattern_safe(char) else char = "%s" end
    return string.match(s, "^(.-)" .. char .. "*$") or s
end

function string.trim_left(s, char)
    if char then char = string.pattern_safe(char) else char = "%s" end
    return string.match(s, "^" .. char .. "*(.+)$") or s
end

function string.pad(str, size, char)
    char = char == nil and " " or char

    local padding = math.floor((size - #str) / 2)
    local extra_padding = (size - #str) % 2

    return string.rep(char, padding) .. str .. string.rep(char, padding + extra_padding)
end

function string.left_pad(str, size, char)
    char = char == nil and " " or char

    local left_padding = size - #str
    return string.rep(char, left_padding) .. str
end

function string.right_pad(str, size, char)
    char = char == nil and " " or char

    local right_padding = size - #str
    return str .. string.rep(char, right_padding)
end

string.lower = utf8.lower
string.upper = utf8.upper
string.escape = utf8.escape

local meta = getmetatable("")

function meta:__index(key)
    local val = string[key]
    if (val ~= nil) then
        return val
    elseif (tonumber(key)) then
        return string.sub(self, key, key)
    end
end

function string.starts_with(str, start)
    return string.sub(str, 1, string.len(start)) == start
end

function string.ends_with(str, endStr)
    return endStr == "" or string.sub(str, -string.len(endStr)) == endStr
end

function table.has(t, x)
    for i,v in ipairs(t) do
        if v == x then
            return true
        end
    end
    return false
end

function table.index(t, x)
    for i,v in ipairs(t) do
        if v == x then
            return i
        end
    end
    return -1
end

function table.remove_value(t, x)
    local index = table.index(t, x)
    if index ~= -1 then
        table.remove(t, index)
    end
end

function table.tostring(t)
    local s = '['
    for i,v in ipairs(t) do
        s = s..tostring(v)
        if i < #t then
            s = s..', '
        end
    end
    return s..']'
end

function file.readlines(path)
    local str = file.read(path)
    local lines = {}
    for s in str:gmatch("[^\r\n]+") do
        table.insert(lines, s)
    end
    return lines
end

local _debug_getinfo = debug.getinfo

function debug.count_frames()
    local frames = 1
    while true do
        local info = _debug_getinfo(frames)
        if info then
            frames = frames + 1
        else
            return frames - 1
        end
    end
end

function debug.get_traceback(start)
    local frames = {}
    local n = 2 + (start or 0)
    while true do
        local info = _debug_getinfo(n)
        if info then
            table.insert(frames, info)
        else
            return frames
        end
        n = n + 1
    end
end

package = {
    loaded = {}
}
local __cached_scripts = {}
local __warnings_hidden = {}

function on_deprecated_call(name, alternatives)
    if __warnings_hidden[name] then
        return
    end
    __warnings_hidden[name] = true
    events.emit("core:warning", "deprecated call", name, debug.get_traceback(2))
    if alternatives then
        debug.warning("deprecated function called ("..name.."), use "..
            alternatives.." instead\n"..debug.traceback())
    else
        debug.warning("deprecated function called ("..name..")\n"..debug.traceback())
    end
end

function reload_module(name)
    local prefix, name = parse_path(name)
    local path = prefix..":modules/"..name..".lua"

    local previous = package.loaded[path]
    if not previous then
        debug.log("attempt to reload non-loaded module "..name.." ("..path..")")
        return
    end
    local script, err = load(file.read(path), path)
    if script == nil then
        error(err)
    end
    local result = script()
    if not result then
        return
    end
    for i, value in ipairs(result) do
        previous[i] = value
    end
    local copy = table.copy(result)
    for key, value in pairs(result) do
        result[key] = nil
    end
    for key, value in pairs(copy) do
        previous[key] = value
    end
end

local internal_locked = false

-- Load script with caching
--
-- path - script path `contentpack:filename`. 
--     Example `base:scripts/tests.lua`
--
-- nocache - ignore cached script, load anyway
local function __load_script(path, nocache, env)
    local packname, filename = parse_path(path)

    if internal_locked and (packname == "res" or packname == "core") 
       and filename:starts_with("modules/internal") then
        error("access to core:internal modules outside of [core]")
    end

    -- __cached_scripts used in condition because cached result may be nil
    if not nocache and __cached_scripts[path] ~= nil then
        return package.loaded[path]
    end
    if not file.isfile(path) then
        error("script '"..filename.."' not found in '"..packname.."'")
    end

    local script, err = load(file.read(path), path)
    if script == nil then
        error(err)
    end
    if env then
        script = setfenv(script, env)
    end
    local result = script()
    if not nocache then
        __cached_scripts[path] = script
        package.loaded[path] = result
    end
    return result
end

function __vc_lock_internal_modules()
    internal_locked = true
end

function require(path)
    if not string.find(path, ':') then
        local prefix, _ = parse_path(_debug_getinfo(2).source)
        return require(prefix .. ':' .. path)
    end
    local prefix, file = parse_path(path)
    local env = __vc__pack_envs[prefix]
    return __load_script(prefix .. ":modules/" .. file .. ".lua", nil, env)
end

function __scripts_cleanup()
    debug.log("cleaning scripts cache")
    for k, v in pairs(__cached_scripts) do
        local packname, _ = parse_path(k)
        if packname ~= "core" then
            debug.log("unloaded "..k)
            __cached_scripts[k] = nil
            package.loaded[k] = nil
        end
    end
end

function __vc__error(msg, frame, n, lastn)
    if events then
        local frames = debug.get_traceback(1)
        events.emit(
            "core:error", msg, 
            table.sub(frames, 1 + (n or 0), lastn and #frames-lastn)
        )
    end
    return debug.traceback(msg, frame)
end

function __vc_warning(msg, detail, n)
    if events then
        events.emit(
            "core:warning", msg, detail, debug.get_traceback(1 + (n or 0)))
    end
end

function file.name(path)
    return path:match("([^:/\\]+)$")
end

function file.stem(path)
    local name = file.name(path)
    return name:match("(.+)%.[^%.]+$") or name
end

function file.ext(path)
    return path:match("%.([^:/\\]+)$")
end

function file.prefix(path)
    return path:match("^([^:]+)")
end

function file.parent(path)
    local dir = path:match("(.*)/")
    if not dir then
        return file.prefix(path)..":"
    end
    return dir
end

function file.path(path)
    local pos = path:find(':')
    return path:sub(pos + 1)
end

function file.join(a, b)
    if a[#a] == ':' then
        return a .. b
    end
    return a .. "/" .. b
end

bit.compile = require "core:bitwise/compiler"
bit.execute = require "core:bitwise/executor"

function __vc_create_random_methods(random_methods)
    local index = 1
    local buffer = nil
    local buffer_size = 64

    local seed_func = random_methods.seed
    local random_func = random_methods.random

    function random_methods:bytes(n)
        local bytes = Bytearray(n)
        for i=1,n do
            bytes[i] = self:random(255)
        end
        return bytes
    end

    function random_methods:seed(x)
        seed_func(self, x)
        buffer = nil
    end

    function random_methods:random(a, b)
        if not buffer or index > #buffer then
            buffer = random_func(self, buffer_size)
            index = 1
        end
        local value = buffer[index]
        if b then
            value = math.floor(value * (b - a + 1) + a)
        elseif a then
            value = math.floor(value * a + 1)
        end

        index = index + 4
        return value
    end
    return random_methods
end
