local cdriver                = require 'plugins.ffi.c-parser.cdriver'
local util                   = require 'plugins.ffi.c-parser.util'
local utility                = require 'utility'
local fs                     = require 'bee.filesystem'
local ws                     = require 'workspace'

local namespace <const>      = 'ffi.namespace*.'

--TODO:supprot 32bit ffi, need config
local knownTypes             = {
    ["bool"] = 'boolean',
    ["char"] = 'integer',
    ["short"] = 'integer',
    ["int"] = 'integer',
    ["long"] = 'integer',
    ["float"] = 'number',
    ["double"] = 'number',
    ["signed"] = 'integer',
    ["__signed"] = 'integer',
    ["__signed__"] = 'integer',
    ["unsigned"] = 'integer',
    ["ptrdiff_t"] = 'integer',
    ["size_t"] = 'integer',
    ["ssize_t"] = 'integer',
    ["wchar_t"] = 'integer',
    ["int8_t"] = 'integer',
    ["int16_t"] = 'integer',
    ["int32_t"] = 'integer',
    ["int64_t"] = 'integer',
    ["uint8_t"] = 'integer',
    ["uint16_t"] = 'integer',
    ["uint32_t"] = 'integer',
    ["uint64_t"] = 'integer',
    ["intptr_t"] = 'integer',
    ["uintptr_t"] = 'integer',
    ["__int8"] = 'integer',
    ["__int16"] = 'integer',
    ["__int32"] = 'integer',
    ["__int64"] = 'integer',
    ["_Bool"] = 'boolean',
    ["__ptr32"] = 'integer',
    ["__ptr64"] = 'integer',
    --[[
    ["_Complex"] = 1,
    ["complex"] = 1,
    ["__complex"] = 1,
    ["__complex__"] = 1,
]]
    ["unsignedchar"] = 'integer',
    ["unsignedshort"] = 'integer',
    ["unsignedint"] = 'integer',
    ["unsignedlong"] = 'integer',
    ["signedchar"] = 'integer',
    ["signedshort"] = 'integer',
    ["signedint"] = 'integer',
    ["signedlong"] = 'integer',
}

local blackKeyWord <const>   = {
    ['and']      = "_and",
    ['do']       = "_do",
    ['elseif']   = "_elseif",
    ['end']      = "_end",
    ['false']    = "_false",
    ['function'] = "_function",
    ['in']       = "_in",
    ['local']    = "_local",
    ['nil']      = "_nil",
    ['not']      = "_not",
    ['or']       = "_or",
    ['repeat']   = "_repeat",
    ['then']     = "_then",
    ['true']     = "_true",
}

local invaildKeyWord <const> = {
    const = true,
    restrict = true,
    volatile = true,
}

local constName <const>      = 'm'

---@class ffi.builder
local builder                = { switch_ast = utility.switch() }

function builder:getTypeAst(name)
    for _, asts in ipairs(self.globalAsts) do
        if asts[name] then
            return asts[name]
        end
    end
end

function builder:needDeref(ast)
    if not ast then
        return false
    end
    if ast.type == 'typedef' then
        -- maybe no name
        ast = ast.def[1]
        if type(ast) ~= 'table' then
            return self:needDeref(self:getTypeAst(ast))
        end
    end
    if ast.type == 'struct' or ast.type == 'union' then
        return true
    else
        return false
    end
end

function builder:getType(name)
    if type(name) == 'table' then
        local t = ""
        local isStruct
        if name.type then
            t = t .. name.type .. "@"
            name = name.name
        end
        for _, part in ipairs(name) do
            local n = part
            if type(n) == 'table' then
                n = n.full_name
            end
            if invaildKeyWord[n] then
                goto continue
            end
            if not isStruct then
                isStruct = self:needDeref(self:getTypeAst(n))
            end
            t = t .. n
            ::continue::
        end
        -- deref 一级指针
        if isStruct and t:sub(#t) == '*' then
            t = t:sub(1, #t - 1)
        end
        name = t
    end
    if knownTypes[name] then
        return knownTypes[name]
    end
    return namespace .. name
end

function builder:isVoid(ast)
    if not ast then
        return false
    end
    if ast.type == 'typedef' then
        return self:isVoid(self:getTypeAst(ast.def[1]) or ast.def[1])
    end

    local typename = type(ast.type) == 'table' and ast.type[1] or ast
    if typename == 'void' then
        return true
    end
    return self:isVoid(self:getTypeAst(typename))
end

local function getArrayType(arr)
    if type(arr) ~= "table" then
        return arr and '[]' or ''
    end
    local res = ''
    for _ in ipairs(arr) do
        res = res .. '[]'
    end
    return res
end

local function getValidName(name)
    return blackKeyWord[name] or name
end

function builder:buildStructOrUnion(lines, tt, name)
    lines[#lines+1] = '---@class ' .. self:getType(name)
    for _, field in ipairs(tt.fields or {}) do
        if field.name and field.type then
            lines[#lines+1] = ('---@field %s %s%s'):format(getValidName(field.name), self:getType(field.type),
                getArrayType(field.isarray))
        end
    end
end

function builder:buildFunction(lines, tt, name)
    local param_names = {}
    for _, param in ipairs(tt.params or {}) do
        local param_name = getValidName(param.name)
        lines[#lines+1] = ('---@param %s %s%s'):format(param_name, self:getType(param.type), getArrayType(param.idxs))
        param_names[#param_names+1] = param_name
    end
    if tt.vararg then
        param_names[#param_names+1] = '...'
    end
    if tt.ret then
        if not self:isVoid(tt.ret) then
            lines[#lines+1] = ('---@return %s'):format(self:getType(tt.ret.type))
        end
    end
    lines[#lines+1] = ('function m.%s(%s) end'):format(name, table.concat(param_names, ', '))
end

function builder:buildTypedef(lines, tt, name)
    local def = tt.def[1]
    if type(def) == 'table' and not def.name then
        -- 这个时候没有主类型，只有一个别名,直接创建一个别名结构体
        self.switch_ast(def.type, self, lines, def, name)
    else
        lines[#lines+1] = ('---@alias %s %s'):format(self:getType(name), self:getType(def))
    end
end

local calculate

local function binop(enumer, val, fn)
    local e1, e2 = calculate(enumer, val[1]), calculate(enumer, val[2])
    if type(e1) == "number" and type(e2) == "number" then
        return fn(e1, e2)
    else
        return { e1, e2, op = val.op }
    end
end
do
    local ops = {
        ['+'] = function (a, b) return a + b end,
        ['-'] = function (a, b) return a - b end,
        ['*'] = function (a, b) return a * b end,
        ['/'] = function (a, b) return a / b end,
        ['&'] = function (a, b) return a & b end,
        ['|'] = function (a, b) return a | b end,
        ['~'] = function (a, b)
            if not b then
                return ~a
            end
            return a ~ b
        end,
        ['<<'] = function (a, b) return a << b end,
        ['>>'] = function (a, b) return a >> b end,
    }
    calculate = function (enumer, val)
        if ops[val.op] then
            return binop(enumer, val, ops[val.op])
        end
        val = util.expandSingle(val)
        if type(val) == "string" then
            if enumer[val] then
                return enumer[val]
            end
            return tonumber(val)
        end
        return val
    end
end

local function pushEnumValue(enumer, name, v)
    v = tonumber(util.expandSingle(v))
    enumer[name] = v
    enumer[#enumer+1] = v
    return v
end

function builder:buildEnum(lines, tt, name)
    local enumer = {}
    for i, val in ipairs(tt.values) do
        local name = val.name
        local v = val.value
        if not v then
            if i == 1 then
                v = 0
            else
                v = tt.values[i - 1].realValue + 1
            end
        end
        if type(v) == 'table' and v.op then
            v = calculate(enumer, v)
        end
        if v then
            val.realValue = pushEnumValue(enumer, name, v)
        end
    end
    local alias = {}
    for k, v in pairs(enumer) do
        alias[#alias+1] = type(k) == 'number' and v or ([['%s']]):format(k)
        if type(k) ~= 'number' then
            lines[#lines+1] = ('m.%s = %s'):format(k, v)
        end
    end
    if name then
        lines[#lines+1] = ('---@alias %s %s'):format(self:getType(name), table.concat(alias, ' | '))
    end
end

builder.switch_ast
    :case 'struct'
    :case 'union'
    :call(builder.buildStructOrUnion)
    :case 'enum'
    :call(builder.buildEnum)
    : case 'function'
    :call(builder.buildFunction)
    :case 'typedef'
    :call(builder.buildTypedef)

local function stringStartsWith(self, searchString, position)
    if position == nil or position < 0 then
        position = 0
    end
    return string.sub(self, position + 1, #searchString + position) == searchString
end
local firstline = ('---@meta \n ---@class %s \n local %s = {}'):format(namespace, constName)
local m = {}
local function compileCode(lines, asts, b)
    for _, ast in ipairs(asts) do
        local tt = ast.type

        if tt.type == 'enum' and not stringStartsWith(ast.name, 'enum@') then
            goto continue
        end
        if not tt.name then
            if tt.type ~= 'enum' then
                goto continue
            end
            --匿名枚举也要创建具体的值
            lines = lines or { firstline }
            builder.switch_ast(tt.type, b, lines, tt)
        else
            tt.full_name = ast.name
            lines = lines or { firstline }
            builder.switch_ast(tt.type, b, lines, tt, tt.full_name)
            lines[#lines+1] = '\n'
        end
        ::continue::
    end
    return lines
end
function m.compileCodes(codes)
    ---@class ffi.builder
    local b = setmetatable({ globalAsts = {}, cacheEnums = {} }, { __index = builder })

    local lines
    for _, code in ipairs(codes) do
        local asts = cdriver.process_context(code)
        if not asts then
            goto continue
        end
        table.insert(b.globalAsts, asts)
        lines = compileCode(lines, asts, b)
        ::continue::
    end
    return lines
end

---@param fileDir fs.path
function m.build_single(codes, fileDir, uri)
    local texts = m.compileCodes(codes)
    if not texts then
        return
    end
    local fullPath = fileDir / ws.getRelativePath(uri)

    if fullPath:stem():string():find '%.' then
        local newPath = fullPath:parent_path() / (fullPath:stem():string():gsub('%.', '/') .. ".lua")
        fs.create_directories(newPath:parent_path())
        fullPath = newPath
    end

    utility.saveFile(tostring(fullPath), table.concat(texts, '\n'))
    return true
end

return m
