local fs             = require 'bee.filesystem'
local time           = require 'bee.time'

local monotonic      = time.monotonic
local osDate         = os.date
local ioOpen         = io.open
local tablePack      = table.pack
local tableConcat    = table.concat
local tostring       = tostring
local debugTraceBack = debug.traceback
local mathModf       = math.modf
local debugGetInfo   = debug.getinfo
local ioStdErr       = io.stderr

local m = {}

m.file = nil
m.startTime = time.time() - monotonic()
m.size = 0
m.maxSize = 100 * 1024 * 1024
m.level = 'info'
m.levelMap = {
    ['trace'] = 1,
    ['debug'] = 2,
    ['info']  = 3,
    ['warn']  = 4,
    ['error'] = 5,
}

local function trimSrc(src)
    if src:sub(1, 1) == '@' then
        src = src:sub(2)
    end
    return src
end

local function init_log_file()
    if not m.file then
        m.file = ioOpen(m.path, 'w')
        if not m.file then
            return
        end
        m.file:write('')
        m.file:close()
        m.file = ioOpen(m.path, 'ab')
        if not m.file then
            return
        end
        m.file:setvbuf 'no'
    end
end

local function pushLog(level, ...)
    if not m.path then
        return
    end
    local t = tablePack(...)
    for i = 1, t.n do
        t[i] = tostring(t[i])
    end
    local str = tableConcat(t, '\t', 1, t.n)
    if level == 'error' then
        str = str .. '\n' .. debugTraceBack(nil, 3)
    end
    local info = debugGetInfo(3, 'Sl')
    local text = m.raw(0, level, str, info.source, info.currentline, monotonic())

    if log.print then
        print(text)
    end

    return text
end

function m.trace(...)
    pushLog('trace', ...)
end

function m.debug(...)
    pushLog('debug', ...)
end

function m.info(...)
    pushLog('info', ...)
end

function m.warn(...)
    pushLog('warn', ...)
end

function m.error(...)
    return pushLog('error', ...)
end

function m.raw(thd, level, msg, source, currentline, clock)
    if m.levelMap[level] < (m.levelMap[m.level] or m.levelMap['info']) then
        return msg
    end
    if level == 'error' then
        ioStdErr:write(msg .. '\n')
        if not m.firstError then
            m.firstError = msg
        end
    end
    if m.size > m.maxSize then
        return msg
    end
    init_log_file()
    local sec, ms = mathModf((m.startTime + clock) / 1000)
    local timestr = osDate('%H:%M:%S', sec)
    local agl = ''
    if #level < 5 then
        agl = (' '):rep(5 - #level)
    end
    local buf
    if currentline == -1 then
        buf = ('[%s.%03.f][%s]%s[#%d]: %s\n'):format(timestr, ms * 1000, level, agl, thd, msg)
    else
        buf = ('[%s.%03.f][%s]%s[#%d:%s:%s]: %s\n'):format(timestr, ms * 1000, level, agl, thd, trimSrc(source), currentline, msg)
    end
    m.size = m.size + #buf
    if m.file then
        if m.size > m.maxSize then
            m.file:write(buf:sub(1, m.size - m.maxSize))
            m.file:write('[REACH MAX SIZE]')
        else
            m.file:write(buf)
        end
    end
    return buf
end

function m.init(root, path)
    local lastBuf
    if m.file then
        m.file:close()
        m.file = nil
        local file = ioOpen(m.path, 'rb')
        if file then
            lastBuf = file:read(m.maxSize)
            file:close()
        end
    end
    m.path = path:string()
    m.prefixLen = #root:string()
    m.size = 0
    pcall(function ()
        if not fs.exists(path:parent_path()) then
            fs.create_directories(path:parent_path())
        end
    end)
    if lastBuf then
        init_log_file()
        if m.file then
            m.file:write(lastBuf)
            m.size = m.size + #lastBuf
        end
    end
end

return m