if (debug.sethook) then
    local table_sort = table.sort
    local string_rep = string.rep
    local string_format = string.format
    local debug_getinfo = debug.getinfo
    local Color = require 'utils.color_presets'
    local Task = require 'utils.task'
    local Token = require 'utils.token'
    local Event = require 'utils.event'

    local Public = {
        call_tree = nil,
        is_running = false
    }

    local stop_profiler_token =
        Token.register(
        function()
            Public.stop()
            game.print('[PROFILER] Stopped!')
            log('[PROFILER] Stopped!')
        end
    )

    -- we can have this on runtime,
    -- but never ever can a player run this without notifying us.
    local allowed = {
        ['Gerkiz'] = true,
        ['mewmew'] = true
    }

    local ignored_functions = {
        [debug.sethook] = true
    }

    local named_sources = {
        ['[string "local n, v = "serpent", "0.30" -- (C) 2012-17..."]'] = 'serpent'
    }

    local function start_command(command)
        local player = game.player
        if player then
            if player ~= nil then
                if not player.admin then
                    local p = player.print
                    p('[ERROR] Only admins are allowed to run this command!', { color = Color.fail })
                    return
                else
                    if allowed[player.name] then
                        Public.start(command.parameter ~= nil)
                    elseif _DEBUG then
                        Public.start(command.parameter ~= nil)
                    end
                end
            end
        end
    end
    local function stop_command(command)
        local player = game.player
        if player then
            if player ~= nil then
                if not player.admin then
                    local p = player.print
                    p('[ERROR] Only admins are allowed to run this command!', { color = Color.fail })
                    return
                else
                    if allowed[player.name] then
                        Public.stop(command.parameter ~= nil, nil)
                    elseif _DEBUG then
                        Public.stop(command.parameter ~= nil, nil)
                    end
                end
            end
        end
    end
    ignored_functions[start_command] = true
    ignored_functions[stop_command] = true

    commands.add_command('start_profiler', 'Starts profiling', start_command)
    commands.add_command('stop_profiler', 'Stops profiling', stop_command)

    --local assert_raw = assert
    --function assert(expr, ...)
    --	if not expr then
    --		Public.stop(false, "Assertion failed")
    --	end
    --	assert_raw(expr, ...)
    --end
    local error_raw = error
    --luacheck: ignore error
    function error(...)
        Public.stop(false, 'Error raised')
        error_raw(...)
    end

    function Public.start(exclude_called_ms)
        if Public.is_running then
            return
        end

        local create_profiler = game.create_profiler

        Public.is_running = true

        Public.call_tree = {
            name = 'root',
            calls = 0,
            profiler = create_profiler(),
            next = {}
        }

        --	Array of Call
        local stack = {[0] = Public.call_tree}
        local stack_count = 0

        debug.sethook(
            function(event)
                local info = debug_getinfo(2, 'nSf')

                if ignored_functions[info.func] then
                    return
                end

                if event == 'call' or event == 'tail call' then
                    local prev_call = stack[stack_count]
                    if exclude_called_ms and prev_call then
                        prev_call.profiler.stop()
                    end

                    local what = info.what
                    local name
                    if what == 'C' then
                        name = string_format('C function %q', info.name or 'anonymous')
                    else
                        local source = info.short_src
                        local namedSource = named_sources[source]
                        if namedSource ~= nil then
                            source = namedSource
                        elseif string.sub(source, 1, 1) == '@' then
                            source = string.sub(source, 1)
                        end
                        name = string_format('%q in %q, line %d', info.name or 'anonymous', source, info.linedefined)
                    end

                    local prev_call_next = prev_call.next
                    if prev_call_next == nil then
                        prev_call_next = {}
                        prev_call.next = prev_call_next
                    end

                    local currCall = prev_call_next[name]
                    local profilerStartFunc
                    if currCall == nil then
                        local prof = create_profiler()
                        currCall = {
                            name = name,
                            calls = 1,
                            profiler = prof
                        }
                        prev_call_next[name] = currCall
                        profilerStartFunc = prof.reset
                    else
                        currCall.calls = currCall.calls + 1
                        profilerStartFunc = currCall.profiler.restart
                    end

                    stack_count = stack_count + 1
                    stack[stack_count] = currCall

                    profilerStartFunc()
                end

                if event == 'return' or event == 'tail call' then
                    if stack_count > 0 then
                        stack[stack_count].profiler.stop()
                        stack[stack_count] = nil
                        stack_count = stack_count - 1

                        if exclude_called_ms then
                            stack[stack_count].profiler.restart()
                        end
                    end
                end
            end,
            'cr'
        )
    end
    ignored_functions[Public.start] = true

    local function dump_tree(averageMs)
        local function sort_Call(a, b)
            return a.calls > b.calls
        end
        local fullStr = {''}
        local str = fullStr
        local line = 1

        local function recurse(curr, depth)
            local sort = {}
            local i = 1
            for k, v in pairs(curr) do
                sort[i] = v
                i = i + 1
            end
            table_sort(sort, sort_Call)

            for ii = 1, #sort do
                local call = sort[ii]

                if line >= 19 then --Localised string can only have up to 20 parameters
                    local newStr = {''} --So nest them!
                    str[line + 1] = newStr
                    str = newStr
                    line = 1
                end

                if averageMs then
                    call.profiler.divide(call.calls)
                end

                str[line + 1] = string_format('\n%s%dx %s. %s ', string_rep('\t', depth), call.calls, call.name, averageMs and 'Average' or 'Total')
                str[line + 2] = call.profiler
                line = line + 2

                local next = call.next
                if next ~= nil then
                    recurse(next, depth + 1)
                end
            end
        end
        if Public.call_tree.next ~= nil then
            recurse(Public.call_tree.next, 0)
            return fullStr
        end
        return 'No calls'
    end

    function Public.stop(averageMs, message)
        if not Public.is_running then
            return
        end

        debug.sethook()

        local text = {'', '\n\n----------PROFILER DUMP----------\n', dump_tree(averageMs), '\n\n----------PROFILER STOPPED----------\n'}
        if message ~= nil then
            text[#text + 1] = string.format('Reason: %s\n', message)
        end
        log(text)
        Public.call_tree = nil
        Public.is_running = false
    end
    ignored_functions[Public.stop] = true

    if _PROFILE then
        Event.on_init(
            function()
                game.print('[PROFILER] Started!')
                log('[PROFILER] Started!')
                Public.start()
                Task.set_timeout_in_ticks(3600, stop_profiler_token)
            end
        )
    end

    return Public
end
