-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local class = require 'mc.class'
local bs = require 'mc.bitstring'
local log = require 'mc.logging'
local context = require 'mc.context'
local skynet = require 'skynet'
local ctx = require 'mc.context'

---@class TopoReader @业务Topo读取
---@field private zone_support table<string, boolean> @当前基础板支持的ZONE区域
---@field private biz_topo_node BusinessTopoNode @TOPO节点对象
local c_topo_reader = class()

local table_cache = require 'mc.table_cache'
local tbl_cache = table_cache.new()

local RETRY_TIMES <const> = 3  -- SMC数据读取异常重试次数

local ZONE_A <const> = 'A'
local ZONE_B <const> = 'B'
local ZONE_C <const> = 'C'
local ZONE_D <const> = 'D'
local ZONE_E <const> = 'E'
local ZONE_F <const> = 'F'

-- TOPO检测SMC命令字
local TOPO_CMD_OFFSET <const> = {
    [ZONE_A] = 0xc005100,
    [ZONE_B] = 0xc005500,
    [ZONE_C] = 0xc005900,
    [ZONE_D] = 0xc005d00,
    [ZONE_E] = 0xc006100,
    [ZONE_F] = 0xc006500
}

-- UBC误码SMC命令字
local UBC_ERROR_CMD_OFFSET <const> = 0xc00f100
local UBC_ERROR_READ_LEN <const> = 8 * 6 -- 每个zone8 Byte，一共6个zone
local RESP_NUM_PER_ZONE <const> = 8       -- 完整的响应数量为8
local UBC_ERROR_RESP_LEN <const> = 1      -- 单条响应数据大小为1 Byte

-- UBC误码检测SMC命令响应体
local ubc_error_resp = bs.new([[<<
    _:1,
    l_eight_l_four:1,
    _:1,
    l_eight_h_four:1,
    _:1,
    h_eight_l_four:1,
    _:1,
    h_eight_h_four:1
>>]])

local function topo_read(chip, offset, len)
    local ok, ret1, ret2
    for _ = 1, RETRY_TIMES do
        ok, ret1 = pcall(function()
            return chip:Read(ctx.new(), offset, len)
        end)
        if not ok then
            log:debug('[BizTopoMonitor] Read topo error, %s', ret1)
            goto next
        end
        skynet.sleep(100) -- 两次读取之间延迟1000ms
        ok, ret2 = pcall(function()
            return chip:Read(ctx.get_context_or_default(), offset, len)
        end)
        if not ok then
            log:debug('[BizTopoMonitor] Read topo error, %s', ret2)
            goto next
        end
        -- 比较两次读取结果，若不同则重新读取
        if ret1 == ret2 then
            break
        else
            ok = false
        end
        ::next::
    end

    return ok, ret1
end

function c_topo_reader:deallocate_ubc_error(ubc_error_info)
    for zone, _ in pairs(ubc_error_info) do
        if ubc_error_info[zone] then
            tbl_cache:deallocate(ubc_error_info[zone])
        end
    end
    tbl_cache:deallocate(ubc_error_info)
end

function c_topo_reader:read_ubc_error()
    local chip = self.biz_topo_node.mds_obj.RefSmcChip
    if not chip then
        error("[BizTopoMonitor] Chip is nil.")
    end

    if not self.zone_support then
        error("[BizTopoMonitor] zone_support is nil")
    end

    local ok, data = topo_read(chip, UBC_ERROR_CMD_OFFSET, UBC_ERROR_READ_LEN)
    if not ok or not data then
        error("[BizTopoMonitor] read topo fail")
    end

    local resp, resp_bin, zone_bin
    local ubc_error_info = tbl_cache:allocate()
    local zone_cnt
    for zone, _ in pairs(TOPO_CMD_OFFSET) do
        zone_cnt = string.byte(zone) - string.byte('A') + 1
        if not self.zone_support[zone] then
            goto next
        end
        zone_bin = string.sub(data, (zone_cnt - 1) * RESP_NUM_PER_ZONE + 1, zone_cnt * RESP_NUM_PER_ZONE)
        ubc_error_info[zone] = tbl_cache:allocate()
        for i = 1, RESP_NUM_PER_ZONE do
            if #data < i * UBC_ERROR_RESP_LEN then
                break
            end
            resp_bin = string.sub(zone_bin, (i - 1) * UBC_ERROR_RESP_LEN + 1, i * UBC_ERROR_RESP_LEN)
            resp = ubc_error_resp:unpack(resp_bin)
            if resp then
                ubc_error_info[zone][zone .. i .. 'a'] = resp.h_eight_h_four
                ubc_error_info[zone][zone .. i .. 'b'] = resp.h_eight_l_four
                ubc_error_info[zone][zone .. i .. 'c'] = resp.l_eight_h_four
                ubc_error_info[zone][zone .. i .. 'd'] = resp.l_eight_l_four
            end
        end
        ::next::
    end

    return ubc_error_info
end

function c_topo_reader:read_topo_info()
    local chip = self.biz_topo_node.mds_obj.RefSmcChip
    if not chip then
        log:error('[BizTopoMonitor] Chip is nil.')
        return
    end
    self:init_support_zone()

    local ctx = context.get_context_or_default()
    ctx.Timeout = 60
    local ok, topo_info = skynet.unpack(chip:PluginRequest(ctx, 'pcie_device', 'plugin_read_topo_info',
            skynet.packstring(self.zone_support)))
    if not ok or not topo_info then
        log:error("[BizTopoMonitor] read topo info fail, position=%s", self.biz_topo_node.position)
        return
    end
    return topo_info
end

function c_topo_reader:init_support_zone()
    -- 识别所有有效的拓扑区域
    for _, c in pairs(self.biz_connectors) do
        for _, port in pairs(c.ports) do
            if not TOPO_CMD_OFFSET[port.zone] then
                log:debug('[BizTopoMonitor] Invalid zone, SrcBizConnector: Slot=%s, position=%s',
                    c.mds_obj.Slot, c.position)
            end
            if string.find(port.name, '^[A-Z][0-9][a-z]$') then
                log:debug("[BizTopoMonitor] set zone support name: %s, zone: %s", port.name, port.zone)
                self.zone_support[port.zone] = true
            end
        end
    end

    return self.zone_support
end

function c_topo_reader:init()
end

---@param biz_connectors BusinessConnector[]
---@param unit_configs UnitConfiguration[]
function c_topo_reader:ctor(biz_topo_node, biz_connectors)
    self.biz_topo_node = biz_topo_node
    self.biz_connectors = biz_connectors
    self.zone_support = {}
end

return c_topo_reader