-- 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.

-- Description: 获取版本
local json = require 'cjson'
local file_sec = require 'utils.file'
local common = require 'external_interface.common'
local log = require 'mc.logging'
local client = require 'manufacture.client'
local mdb = require 'mc.mdb'
local context = require 'mc.context'

local function_get_version = {}

local FIRMWARE_BIOS_ID = "Bios"
local FIRMWARE_ACTIVE_UBOOT_ID = "ActiveUboot"
local FIRMWARE_BACKUP_UBOOT_ID = "BackupUboot"
local FIRMWARE_PSR_ID = "SR_PSR"
local FIRMWARE_VRD_BCU_ID = "VRD_BCU"
local FIRMWARE_BMC_MAIN_ID = "ActiveBMC"

local PCBVERSION = "PcbVersion"
local CPLDVERSION = "LogicVersion"
local SRVERSION = "SRVersion"
local MCUVERSION = "MCUVersion"
local PSIPVERSION = "PSIPVersion"
local FIRMWARE_COPROCESSOR_ID = "CoprocessorFirmware"

local CPLDUNIT = "LogicUnit"

local PCIECARD_INTERFACE = 'bmc.kepler.Systems.PCIeDevices.PCIeCard'
local CPUBOARD_INTERFACE<const> = 'bmc.kepler.Systems.Board.CpuBoard'
local BMC_VER_PATH<const> = '/etc/version.json'

-- 依次从networkAdapter中获取 （指定）版本
local function get_prop_from_adapter(fruid, version_type)
    local version = ''
    local obj_list = client:GetNetworkAdapterObjects()
    if not obj_list or not next(obj_list) then
        log:error("get networkAdapter objects failed")
        return version
    end
    for _, obj in pairs(obj_list) do
        if fruid == obj.FruId then
            version = version_type and obj[version_type] or ''
            break
        end
    end
    return version
end

-- 依次从board中获取 （指定）版本、（指定）位号
local function get_prop_from_board(fruid, version_type, unit_type)
    local version = ''
    local unit = ''
    local obj_list = client:GetBoardObjects()
    if not obj_list or not next(obj_list) then
        log:error("get Board objects failed")
        return version, unit
    end
    for _, obj in pairs(obj_list) do
        if fruid == obj.FruID then
            version = version_type and obj[version_type] or ''
            unit = unit_type and obj[unit_type] or ''
            break
        end
    end
    return version, unit
end

-- 获取表元素个数
local function get_tbl_num(tbl)
    local count = 0
    if type(tbl) ~= "table" then
        return count
    end
    for _, _ in pairs(tbl) do
        count = count + 1
    end
    return count
end

local function generate_multi_cpld_ver_str(mul_ver, mul_unit)
    local result = ''
    local count = 0
    for i = 1, get_tbl_num(mul_ver) do
        if not mul_unit['CPLD' .. i] or not mul_ver['CPLD' .. i] then
            goto continue
        end
        count = count + 1
        if i == get_tbl_num(mul_ver) then
            result = string.format('%s(U%s)%s', result,
                mul_unit['CPLD' .. i], mul_ver['CPLD' .. i])
        else
            result = string.format('%s(U%s)%s;', result,
                mul_unit['CPLD' .. i], mul_ver['CPLD' .. i])
        end
        ::continue::
    end
    return count > 1 and result or nil
end

local function generate_fpga_ver_str(mul_ver, mul_unit)
    local result = ''
    if mul_unit['FPGA'] and mul_ver['FPGA'] then
        result = string.format('(U%s)%s', mul_unit['FPGA'], mul_ver['FPGA'])
    end
    return result
end

local generate_multi_ver_str = {
    ["CPLD"] = generate_multi_cpld_ver_str,
    ["FPGA"] = generate_fpga_ver_str
}

-- 从board获取多cpld版本和位号
local function get_multi_logic_ver_unit(fruid, logic_type)
    local obj_list = client:GetBoardObjects()
    if not obj_list or not next(obj_list) then
        log:error("get Board objects failed")
        return
    end
    for _, obj in pairs(obj_list) do
        if fruid == obj.FruID then
            local mul_unit = obj['MultiLogicUnit']
            local mul_ver = obj['MultiLogicVersion']
            if not mul_unit or not mul_ver or
                get_tbl_num(mul_unit) <= 1 or get_tbl_num(mul_ver) <= 1 then
                return
            end
            return generate_multi_ver_str[logic_type](mul_ver, mul_unit)
        end
    end
end

-- 依次从frimware中获取 版本、位号、发行日期
local function get_prop_from_firmware(fruid, id)
    local version = ''
    local unit = ''
    local release_date = ''
    local obj_list = client:GetFirmwareInfoObjects()
    if fruid ~= 0 then
        return version, unit, release_date
    end
    if not obj_list or not next(obj_list) then
        log:error("get firmware objects failed")
        return version, unit, release_date
    end
    for _, obj in pairs(obj_list) do
        if string.find(obj.Id, id) ~= nil then
            version = obj.Version
            unit = obj.Location
            release_date = obj.ReleaseDate
            break
        end
    end
    return version, unit, release_date
end

-- 从文件中获取BMC的对内版本号
local function get_bmc_general_version(fruid)
    local file = file_sec.open_s(BMC_VER_PATH, 'r')
    if not file then
        log:error('base version file path is invalid.')
        return get_prop_from_firmware(fruid, FIRMWARE_BMC_MAIN_ID)
    end
 
    local ok, json_data = pcall(json.decode, file:read('*a'))
    file:close()
    if not ok or not json_data then
        log:error('get base version file content failed, error is %s.', json_data)
        return get_prop_from_firmware(fruid, FIRMWARE_BMC_MAIN_ID)
    end
 
    return json_data.Version or ''
end

local function get_board_object(fruid)
    local obj_list = client:GetBoardObjects()
    if not obj_list or not next(obj_list) then
        log:error("get board objects failed")
        return
    end
    for _, obj in pairs(obj_list) do
        if obj.FruID == fruid then
            return obj
        end
    end
end

local function get_firmware_obj(id, host_id)
    local host_id_last= ""
    local firmware_obj_list = client:GetFirmwareInfoObjects()
    if not firmware_obj_list or not next(firmware_obj_list) then
        log:error("get firmware objects failed")
        return
    end
    if host_id ~= 1 then
        host_id_last = host_id
    end
    for _, obj in pairs(firmware_obj_list) do
        if obj.Id == id .. host_id_last then
            return obj
        end
    end
end

-- 依次从firmware中获取 版本、位号、发行日期
local function get_prop_from_bios_firmware(fruid, id, host_id)
    local version = ''
    local unit = ''

    if not fruid or fruid == 0 then
        local firmware_obj = get_firmware_obj(id, host_id)
        version = firmware_obj and firmware_obj.Version or version
        unit = firmware_obj and firmware_obj.Location or unit
        return version, unit
    end

    local board_obj = get_board_object(fruid)
    if not board_obj then
        return version, unit
    end

    local ok, cpu_board_obj = pcall(
        mdb.try_get_object, client:get_bus(), board_obj.path, CPUBOARD_INTERFACE)
    if not ok then
        log:error("get cpuboard object failed, error: %s", cpu_board_obj)
        return version, unit
    end

    version = cpu_board_obj.BIOSVersion
    unit = 'U75'
    return version, unit
end

--根据cpuboard的DeviceName来匹配vrd对象
local function get_prop_vrd_to_fruid(fruid, id)
    local version = ''
    local obj_Board_list = client:GetBoardObjects()
    local obj_firmware_list = client:GetFirmwareInfoObjects()
    local device_name = ''
    if not obj_firmware_list or not next(obj_firmware_list) then
        log:error("get firmware objects failed")
        return version
    end
    if not obj_Board_list or not next(obj_Board_list) then
        log:error("get Board objects failed")
        return version
    end
    for _, obj in pairs(obj_Board_list) do
        if fruid == obj.FruID then
            device_name = obj.DeviceName
            break
        end
    end
    for _, obj in pairs(obj_firmware_list) do
        if string.find(obj.Id, id) ~= nil then
            if obj.Name == device_name .. " VRD" then
                version = obj.Version
                return version
            end
        end
    end
    return version
end

local function get_pcb_version(fruid)
    local version = get_prop_from_board(fruid, PCBVERSION)
    if version == '' then
        version = get_prop_from_adapter(fruid, "PCBVersion")
    end
    return version
end

local function get_bmc_main_version(fruid)
    return get_bmc_general_version(fruid)
end

local function get_prop_from_network(fruid, version_type, unit_type)
    local dpu_component = {}
    client:ForeachComponentObjects(function (obj)
        if fruid == obj['FruId'] then
            dpu_component = obj
        end
    end)
    -- 当前根据type是不是57判断是不是SDI卡
    if dpu_component.Type ~= 57 then
        return '', ''
    end
    local dpu_obj_list = client:GetDPUCardObjects()
    if not dpu_obj_list or not next(dpu_obj_list) then
        log:info("get DPUCard objects failed")
        return '', ''
    end
    for p, dpu_obj in pairs(dpu_obj_list) do
        -- 根据SerialNumber找对应的dpu对象。
        local ok, pcie_obj = pcall(mdb.get_object, client:get_bus(), p, PCIECARD_INTERFACE)
        if ok and pcie_obj.SerialNumber == dpu_component.SerialNumber then
            log:notice('version: %s', version_type)
            log:notice('dpu_obj[version_type]: %s', dpu_obj[version_type])
            local version = version_type and dpu_obj[version_type] or ''
            local unit = unit_type and dpu_obj[unit_type] or ''
            return version, unit
        end
    end
    return '', ''
end

local function get_cpld_version(fruid)
    local version = ''
    local cpld_version, unit = get_prop_from_board(fruid, CPLDVERSION, CPLDUNIT)
    if cpld_version == "" then
        -- 添加dpu卡获取流程
        cpld_version = get_prop_from_network(fruid, CPLDVERSION)
        unit = 20  -- SDI6.0 U位号固定为20
    end
    local multi_ver = get_multi_logic_ver_unit(fruid, "CPLD")
    if cpld_version ~= "" then
        version = string.format("(U%s)%s", unit, cpld_version)
    end
    version = multi_ver or version
    return version
end

local function get_fpga_version(fruid)
    local version = get_multi_logic_ver_unit(fruid, "FPGA")
    return version or ""
end

local function get_bios_version(fruid, HostId)
    local host_id = HostId or 1
    local version = ''
    local bios_version, location = get_prop_from_bios_firmware(fruid, FIRMWARE_BIOS_ID, host_id)
    if bios_version ~= "" then
        version = string.format("(%s)%s", location, bios_version)
    end
    return version
end

local function get_sdr_version()
    return "1.50"  -- sdr概念已经不再使用，版本号返回固定值即可
end

local function get_uboot_version(fruid)
    local version, _, release_date = get_prop_from_firmware(fruid, FIRMWARE_ACTIVE_UBOOT_ID)
    if version ~= "" then
            version = string.format("%s(%s)", version, release_date)
    end
    return version
end

local function get_uboot_backup_version(fruid)
    local version, _, release_date = get_prop_from_firmware(fruid, FIRMWARE_BACKUP_UBOOT_ID)
    if version ~= "" then
            version = string.format("%s(%s)", version, release_date)
    end
    return version
end

local function get_bmc_backup_version(fruid)
    return get_bmc_general_version(fruid)
end

local function get_bmc_available_version(fruid)
    if fruid ~= 0 then
        return ''
    end

    local ok, ret = client:PManagerVersionManagerVersionGetReleaseVersion(context.get_context_or_default())
    if not ok or not ret.AvailableReleaseVersion then
        return ''
    end
    return ret.AvailableReleaseVersion
end

local function get_sr_version(fruid)
    if fruid == 0 then
        return get_prop_from_firmware(fruid, FIRMWARE_PSR_ID)
    else
        local version = get_prop_from_board(fruid, SRVERSION)
        if version == "" then
            -- 添加dpu卡获取流程
            version = get_prop_from_network(fruid, SRVERSION)
        end
        return version
    end
end

local function get_vrd_version(fruid)
    local version = ""
    if fruid == 0 then
        version = get_prop_from_firmware(fruid, FIRMWARE_VRD_BCU_ID)
    else
        version = get_prop_vrd_to_fruid(fruid, FIRMWARE_VRD_BCU_ID)
    end
    return version
end

local function get_mcu_version(fruid)
    local version = get_prop_from_board(fruid, MCUVERSION)
    if version == "" then
        -- 添加dpu卡获取流程
        version = get_prop_from_network(fruid, MCUVERSION)
    end
    return version
end

local function get_psip_version(fruid)
    return get_prop_from_board(fruid, PSIPVERSION)
end

local function get_copro_version(fruid)
    return get_prop_from_firmware(fruid, FIRMWARE_COPROCESSOR_ID)
end

function_get_version.get_version={
    [common.PCB_VERSION] = get_pcb_version,
    [common.BMC_MAIN_VERSION] = get_bmc_main_version,
    [common.CPLD_VERSION] = get_cpld_version,
    [common.FPGA_VERSION] = get_fpga_version,
    [common.BIOS_VERSION] = get_bios_version,
    [common.SDR_VERSION] = get_sdr_version,
    [common.UBOOT_VERSION] = get_uboot_version,
    [common.BMC_BACKUP_VERSION] = get_bmc_backup_version,
    [common.UBOOT_BACKUP_VERSION] = get_uboot_backup_version,
    [common.BMC_AVAILABLE_VERSION] = get_bmc_available_version,
    [common.SR_VERSION] = get_sr_version,
    [common.VRD_VERSION] = get_vrd_version,
    [common.MCU_VERSION] = get_mcu_version,
    [common.PSIP_VERSION] = get_psip_version,
    [common.COPROCESSOR_VERSION] = get_copro_version
}

function function_get_version.get_version_by_type(fruid, ver_type, ctx)
    if not function_get_version.get_version[ver_type] then
        return ""
    end
    return function_get_version.get_version[ver_type](fruid, ctx.HostId)
end

function function_get_version.get_all_version(fruid, ctx)
    local version_string = ""
    for version_type, func in pairs(function_get_version.get_version) do
        local version = func(fruid, ctx and ctx.HostId)
        if version ~= "" then
            version_string = version_string ..
                string.format("%s: %s\r\n", common.version_key_list[version_type], version)
        end
    end
    version_string = string.sub(version_string, 1, -3)
    return version_string
end

return function_get_version