local util = require "util"

local fmt_file_header = [[
// Code generated by sprotodump
// source: %s
// DO NOT EDIT!

/*
    Package %s is a generated sproto package.
*/
package %s

import (
    "reflect"

    "github.com/xjdrew/gosproto"
)

// avoids "imported but not used"
var _ reflect.Type
]]

local fmt_struct_header = [[type %s struct {]]
local fmt_struct_field = [[%s %s `%s`]]
local fmt_struct_end = [[}
]]

-- protocol name
local fmt_protocol_name = [[var Name string = "%s"]]

-- protocols
local fmt_protocols_header = [[var Protocols []*sproto.Protocol = []*sproto.Protocol{]]
local fmt_protocols_end = [[}]]

-- protocol request and response
local fmt_protocol = {
    zz = [[&sproto.Protocol{
            Type: %d,
            Name: "%s",
            MethodName: "%s",
            Request: reflect.TypeOf(&%s{}),
            Response: reflect.TypeOf(&%s{}),
        },]],
    za = [[&sproto.Protocol{
            Type: %d,
            Name: "%s",
            MethodName: "%s",
            Request: reflect.TypeOf(&%s{}),
        },]],
    az = [[&sproto.Protocol{
            Type: %d,
            Name: "%s",
            MethodName: "%s",
            Response: reflect.TypeOf(&%s{}),
        },]],
    aa = [[&sproto.Protocol{
            Type: %d,
            Name: "%s",
            MethodName: "%s",
        },]]
}

local stream = {}
stream.__index = stream

function stream:write(str)
    table.insert(self.lines, str)
end

function stream:dump()
    return table.concat(self.lines, "\n")
end

local function new_stream()
    local obj = {
        lines = {}
    }
    return setmetatable(obj, stream)
end

local function get_base_name(filename)
    return string.match(filename, "([%a_]+).sproto$")
end

local function get_package_name(filename)
    local name = get_base_name(filename)
    return ("sproto_%s"):format(name)
end

local function get_file_header(filename, param)
    local package = param.package or get_package_name(filename)
    return fmt_file_header:format(filename, package, package)
end

local function canonical_name(name)
    return name:gsub("%f[^\0%_%.]%l",string.upper):gsub("[%_%.]","")
end

local function map_item_field_name(field)
    return ("mapItem%s"):format(canonical_name(field.name))
end

local type_map = {
    string = "*string",
    integer = "*int64",
    boolean = "*bool",
    double = "*float64",
    binary = "[]byte",
}

local array_type_map = {
    string = "[]string",
    integer = "[]int64",
    boolean = "[]bool",
    double = "[]float64",
    binary = "[][]byte",
}

local key_type_map = {
    string = "string",
    integer = "int64",
    boolean = "bool",
    double = "float64",
    -- go的map不支持以slice做key，所以这里不支持binary类型
}

local function get_type_string(field)
    local target
    if field.array then
        target = array_type_map[field.typename]
        if not target then
            if field.map or (field.key and field.key ~= "") then  -- map
                -- map key一定是基础类型
                local key_type = key_type_map[field.map_keyfield.typename]
                assert(key_type, field.typename .. ":" .. field.map_keyfield.typename)

                local value_type = false
                if field.key and field.key ~= "" then  -- array with index key
                    -- map_valuefield一定是struct
                    assert(not type_map[field.map_valuefield.typename], field.map_valuefield.typename)
                    value_type = string.format("*%s", canonical_name(field.map_valuefield.typename))
                else
                    -- map_valuefield 可能是builtin类型，也可能是struct
                    value_type = get_type_string(field.map_valuefield)

                    -- 无法处理嵌套递归的类型，比如：
                    --[[
                        .Nested {
                            id 1 : int
                            nested 2 : *Nested()
                        }
                        .Struct {
                            m 1 : *Nested()
                        }
                    ]]
                    -- 这里 Struct.m 转换成go类型就是 map[int]map[int]map[int]... 无限递归下去
                    -- sprotodump也会因此 stack overflow
                    -- 一个可能的解决方法是定义成 map[int]interface{} ，动态创建嵌套的map
                end

                target = string.format("map[%s]%s", key_type, value_type)
            else  -- array
                target = string.format("[]*%s", canonical_name(field.typename))
            end
        end
    else
        target = type_map[field.typename]
        if not target then
            target = string.format("*%s", canonical_name(field.typename))
        end
    end
    return assert(target, field.typename .. ":" .. tostring(field.array))
end

local function get_sproto_meta_string(field)
    local meta = {}
    if type_map[field.typename] then
        table.insert(meta, field.typename)
    else
        table.insert(meta, "struct")
    end

    table.insert(meta, field.tag)

    if field.array then
        table.insert(meta, "array")
    end

    if field.key and field.key ~= "" then -- array with index key
        table.insert(meta, ("key=%d"):format(field.map_keyfield.tag))
    elseif field.map then -- map
        table.insert(meta, ("key=%d"):format(field.map_keyfield.tag))
        table.insert(meta, ("value=%d"):format(field.map_valuefield.tag))
        table.insert(meta, ("subtype=%s"):format(map_item_field_name(field)))
    end

    return ("sproto:\"%s\""):format( table.concat(meta, ",") )
end

local function get_json_meta_string(field)
    local meta = {}
    table.insert(meta, field.name)
    return ("json:\"%s\""):format( table.concat(meta, ",") )
end

local function write_struct_field(f, field)
    local name = canonical_name(field.name)
    local typename = get_type_string(field)
    local tags = table.concat({get_sproto_meta_string(field), get_json_meta_string(field)}, " ")
    f:write(fmt_struct_field:format(name, typename, tags))
end

local function write_map_item_field(f, field)
    if not field.map then
        return
    end
    local name = map_item_field_name(field)
    local typename = canonical_name(field.typename)
    local tags = "json:\"-\""
    f:write(("%s *%s `%s`"):format(name, typename, tags))
end

local function write_struct(f, name, fields)
    local name = canonical_name(name)
    f:write(fmt_struct_header:format(name))
    for _, field in ipairs(fields) do
        write_struct_field(f, field)
    end
    for _, field in ipairs(fields) do
        write_map_item_field(f, field)
    end
    f:write(fmt_struct_end)
end

local function write_protocol(f, name, basename, protocol)
    local fullname = string.format("%s.%s", basename, name)
    local method_name = string.format("%s.%s", canonical_name(basename), canonical_name(name))
    local params = {protocol.tag, fullname, method_name}
    local key
    if protocol.request then
        local request = canonical_name(protocol.request)
        table.insert(params, request)
        key = "z"
    else
        key = "a"
    end

    if protocol.response then
        local response = canonical_name(protocol.response)
        table.insert(params, response)
        key = key .. "z"
    else
        key = key .. "a"
    end
    f:write(fmt_protocol[key]:format(table.unpack(params)))
end

local function sort_by_meta(set)
    local arr = {}
    for name, obj in pairs(set) do
        table.insert(arr, {name=name, obj=obj, meta=obj.meta})
    end
    table.sort(arr, function(a, b)
        if a.meta.file ~= b.meta.file then
            return a.meta.file < b.meta.file
        end
        if a.meta.line ~= b.meta.file then
            return a.meta.line < b.meta.line
        end
        -- order可能缺失，只存在于protocol的request和response结构体
        if a.meta.order and b.meta.order then
            return a.meta.order < b.meta.order
        end
        return false
    end)
    return arr
end

local function main(trunk, build, param)
    assert(#param.sproto_file==1, "one sproto file one package")
    local f = new_stream()
    local filename = param.sproto_file[1]
    f:write(get_file_header(filename, param))

    local sorted_structs = sort_by_meta(trunk[1].type)
    for _, obj in pairs(sorted_structs) do
        local name = obj.name
        local fields = obj.obj
        write_struct(f, name, fields)
    end

    local base_name = get_base_name(filename)
    f:write(fmt_protocol_name:format(base_name))
    f:write(fmt_protocols_header)
    local sorted_protocols = sort_by_meta(trunk[1].protocol)
    for _, obj in pairs(sorted_protocols) do
        local name = obj.name
        local protocol = obj.obj
        write_protocol(f, name, base_name, protocol)
    end
    f:write(fmt_protocols_end)

    local content = f:dump()

    local outfile = param.outfile
    if not outfile then
        print(content)
        return
    end

    local dir = param.dircetory or ""
    local file = dir .. outfile
    util.write_file(file, content, "w")
end

return main
