-- 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 ipmi = require 'ipmi'
local bs = require 'mc.bitstring'
local log = require 'mc.logging'
local enums = require 'ipmi.enums'
local vrd_enums = require 'vrd.enum.vrd_enums'
local vrd_mgmt = require 'vrd.vrd_mgmt_obj'
local client = require 'general_hardware.client'

local channel_type = enums.ChannelType
local comp_code = ipmi.types.Cc

local max_retry<const> = 5
local SMBIOS_WRITE_FINISH<const> = 3

local vrd_object = {}
vrd_object.__index = vrd_object

function vrd_object.new(obj, position)
    return setmetatable({vrd = obj, retry = 0, voltage_info_table = {}, temperature_info_table = {},
        position = position}, vrd_object)
end

local reqGetVrdInfo = bs.new('<<0xDB0700:3/unit:8, 0x37, CpuId>>')
local rspGetVrdInfo = bs.new('<<0xDB0700:3/unit:8, CpuId, PowerNum, InfoSize, Info/binary>>')
local data_parser_per_power = bs.new(
    '<<type, dieid, volt_low, volt_high, cur_low, cur_mid, cur_high, temp:16, info/binary>>')

local function check_vrd_type(type)
    if type == vrd_enums.POWER_TYPE.SERDES or type == vrd_enums.POWER_TYPE.IO_DVDD09 then
        return true
    end
    if type <= vrd_enums.POWER_TYPE.HBM and type >= vrd_enums.POWER_TYPE.CORE then
        return true
    end
    return false
end

local function check_vrd_dieid(dieid)
    if dieid == vrd_enums.POWER_DIEID.D0_NOT_EXIST or dieid == vrd_enums.POWER_DIEID.D1_NOT_EXIST then
        return true
    end
    if dieid <= vrd_enums.POWER_DIEID.TD and dieid >= vrd_enums.POWER_DIEID.NA then
        return true
    end
    return false
end
    
function vrd_object:parse_vrd_rsp_info(payload)
    if not payload then
        log:info('parse_vrd_rsp_info payload is nil')
        return comp_code.ResponseError
    end
    local rsp = rspGetVrdInfo:unpack(payload)
    if rsp == nil then
        log:info('parse_vrd_rsp_info rsp is nil')
        return comp_code.ResponseError
    end
    local power_num, info_size, power_info = rsp.PowerNum, rsp.InfoSize, rsp.Info
    local i = 0
    local data_per_power, voltage, temperature
    local type, dieid, volt_low, volt_high, cur_low, cur_mid, cur_high, tmp_power_info
    log:debug('power_num :%s, info_size :%s, power_info_size :%s', power_num, info_size, #power_info)
    while i < power_num and power_info ~= nil and #power_info ~= 0 do
        data_per_power = data_parser_per_power:unpack(power_info)
        if not data_per_power then
            log:info('parse_vrd_rsp_info data_per_power is nil')
            return comp_code.ResponseError
        end
        type, dieid, volt_low, volt_high, cur_low, cur_mid, cur_high, tmp_power_info =
            data_per_power.type, data_per_power.dieid, data_per_power.volt_low,
            data_per_power.volt_high, data_per_power.cur_low, data_per_power.cur_mid,
            data_per_power.cur_high, data_per_power.info
        if dieid ~= 0xff and not check_vrd_type(type) and not check_vrd_dieid(dieid) then
            log:info('type(%s) or dieid(%s) error', type, dieid)
            return comp_code.ResponseError
        else
            voltage = tonumber(string.format('%d.%02d', volt_high, volt_low))
            if not self.voltage_info_table[type + 1] then
                self.voltage_info_table[type + 1] = {}
            end
            self.voltage_info_table[type + 1][dieid + 1] = voltage

            temperature = data_per_power.temp
            if not self.temperature_info_table[type + 1] then
                self.temperature_info_table[type + 1] = {}
            end
            self.temperature_info_table[type + 1][dieid + 1] = temperature
        end
        power_info = tmp_power_info
        i = i + 1
    end
    return comp_code.Success
end

function vrd_object:get_vrd_info_from_imu(bus, cpu_id)
    local cpu_obj = vrd_mgmt.get_cpu_obj_with_cpuid(cpu_id)
    if cpu_obj == nil then
        log:error('get cpu obj failed')
        return nil
    end
    -- ipmb消息CpuId为0、1对应CPU1、CPU2
    local data = reqGetVrdInfo:pack({CpuId = cpu_obj.LogicalId})
    local ok, cc, payload = pcall(ipmi.request, bus, channel_type.CT_ME:value(),
        {DestNetFn = 0x30, Cmd = 0x98, Payload = data})
    if not ok then
        log:debug('Send request by ipmi fail, err_message: %s', cc)
        return nil
    end
    if cc == comp_code.Success then
        local cc = self:parse_vrd_rsp_info(payload)
        return cc
    end
    return cc
end

-- 无效值为255.000
function vrd_object:set_vrd_to_invalid()
    self.vrd.Status = 1

    self.vrd.Cpu0v9TACore = 255.000
    self.vrd.Cpu0v75DDRVDD = 255.000
    self.vrd.Cpu0v9TBCore = 255.000
    self.vrd.Cpu0v9Uncore = 255.000
    self.vrd.Cpu0v8NADVDD = 255.000
    self.vrd.Cpu0v8NBDVDD = 255.000
    self.vrd.Cpu1v1DDRVddq = 255.000

    self.vrd.CpuTACoreTemp = 0xFFFF
    self.vrd.CpuDDRVDDTemp = 0xFFFF
    self.vrd.CpuTBCoreTemp = 0xFFFF
    self.vrd.CpuUncoreTemp = 0xFFFF
    self.vrd.CpuNADVDDTemp = 0xFFFF
    self.vrd.CpuNBDVDDTemp = 0xFFFF
    self.vrd.CpuDDRVddqTemp = 0xFFFF

    -- os上电状态才置为异常值
    local smbios_objs = client:GetSmBiosObjects()
    for _, smbios_obj in pairs(smbios_objs) do
        if smbios_obj.SmbiosStatus == SMBIOS_WRITE_FINISH then
            self.vrd.VrdTemperatureCelsius = 0x7EFF
        end
    end
end

-- 当不在位时重置为无效值
function vrd_object:reset_vrd_info()
    -- 电压传感器0或255失效,255会上报获取失败告警
    self.vrd.Cpu0v9TACore = 0
    self.vrd.Cpu0v75DDRVDD = 0
    self.vrd.Cpu0v9TBCore = 0
    self.vrd.Cpu0v9Uncore = 0
    self.vrd.Cpu0v8NADVDD = 0
    self.vrd.Cpu0v8NBDVDD = 0
    self.vrd.Cpu1v1DDRVddq = 0

    -- 温度传感器大于255失效,无异常失败告警
    self.vrd.CpuTACoreTemp = 0xFFFF
    self.vrd.CpuDDRVDDTemp = 0xFFFF
    self.vrd.CpuTBCoreTemp = 0xFFFF
    self.vrd.CpuUncoreTemp = 0xFFFF
    self.vrd.CpuNADVDDTemp = 0xFFFF
    self.vrd.CpuNBDVDDTemp = 0xFFFF
    self.vrd.CpuDDRVddqTemp = 0xFFFF
    self.vrd.VrdTemperatureCelsius = 0xFFFF
    self.vrd.Status = 0
    self.retry = 0
end

function vrd_object:update_max_temp()
    local max_temp = 0
    for _, temp_type in pairs(self.temperature_info_table) do 
        for _, temp_die in pairs(temp_type) do 
            max_temp = temp_die > max_temp and temp_die or max_temp
        end
    end
    return max_temp
end

function vrd_object:update_vrd_voltage()
    for type = 0, vrd_enums.POWER_TYPE.HBM do
        if not self.voltage_info_table[type + 1] then
            self.voltage_info_table[type + 1] = {}
        end
    end
    -- 未获取到对应类型电压时不更新属性
    self.vrd.Cpu0v9TACore =
        self.voltage_info_table[vrd_enums.POWER_TYPE.CORE + 1][vrd_enums.POWER_DIEID.TA + 1] or
            self.vrd.Cpu0v9TACore
    self.vrd.Cpu0v75DDRVDD =
        self.voltage_info_table[vrd_enums.POWER_TYPE.DDR + 1][vrd_enums.POWER_DIEID.NOT_EXIST + 1] or
            self.vrd.Cpu0v75DDRVDD
    self.vrd.Cpu0v9TBCore =
        self.voltage_info_table[vrd_enums.POWER_TYPE.CORE + 1][vrd_enums.POWER_DIEID.TB + 1] or
            self.vrd.Cpu0v9TBCore
    self.vrd.Cpu0v9Uncore =
    self.voltage_info_table[vrd_enums.POWER_TYPE.UNCORE + 1][vrd_enums.POWER_DIEID.NOT_EXIST + 1] or
        self.vrd.Cpu0v9Uncore
    self.vrd.Cpu0v8NADVDD =
        self.voltage_info_table[vrd_enums.POWER_TYPE.Nimbus + 1][vrd_enums.POWER_DIEID.NA + 1] or self.vrd.Cpu0v8NADVDD
    self.vrd.Cpu0v8NBDVDD =
        self.voltage_info_table[vrd_enums.POWER_TYPE.Nimbus + 1][vrd_enums.POWER_DIEID.NB + 1] or self.vrd.Cpu0v8NBDVDD
    self.vrd.Cpu1v1DDRVddq =
        self.voltage_info_table[vrd_enums.POWER_TYPE.VDDQ + 1][vrd_enums.POWER_DIEID.NOT_EXIST + 1] or
            self.vrd.Cpu1v1DDRVddq
end

function vrd_object:update_vrd_temp()
    for type = 0, vrd_enums.POWER_TYPE.HBM do 
        if not self.temperature_info_table[type + 1] then
            self.temperature_info_table[type + 1] = {}
        end
    end
    -- 未获取到对应类型温度时不更新属性
    self.vrd.CpuTACoreTemp =
        self.temperature_info_table[vrd_enums.POWER_TYPE.CORE + 1][vrd_enums.POWER_DIEID.TA + 1] or
            self.vrd.CpuTACoreTemp
    self.vrd.CpuDDRVDDTemp =
        self.temperature_info_table[vrd_enums.POWER_TYPE.DDR + 1][vrd_enums.POWER_DIEID.NOT_EXIST + 1] or
            self.vrd.CpuDDRVDDTemp
    self.vrd.CpuTBCoreTemp =
        self.temperature_info_table[vrd_enums.POWER_TYPE.CORE + 1][vrd_enums.POWER_DIEID.TB + 1] or
            self.vrd.CpuTBCoreTemp
    self.vrd.CpuUncoreTemp =
        self.temperature_info_table[vrd_enums.POWER_TYPE.UNCORE + 1][vrd_enums.POWER_DIEID.NOT_EXIST + 1] or
            self.vrd.CpuUncoreTemp
    self.vrd.CpuNADVDDTemp =
        self.temperature_info_table[vrd_enums.POWER_TYPE.Nimbus + 1][vrd_enums.POWER_DIEID.NA + 1] or
            self.vrd.CpuNADVDDTemp
    self.vrd.CpuNBDVDDTemp =
        self.temperature_info_table[vrd_enums.POWER_TYPE.Nimbus + 1][vrd_enums.POWER_DIEID.NB + 1] or
            self.vrd.CpuNBDVDDTemp
    self.vrd.CpuDDRVddqTemp =
        self.temperature_info_table[vrd_enums.POWER_TYPE.VDDQ + 1][vrd_enums.POWER_DIEID.NOT_EXIST + 1] or
            self.vrd.CpuDDRVddqTemp
end


function vrd_object:update_vrd_info(bus)
    self.retry = self.retry + 1
    local cc = self:get_vrd_info_from_imu(bus, self.vrd.CpuId)
    if cc == comp_code.Success then
        -- 重置retry count和状态
        self.retry = 0
        self.wait_for_init = false
        local max_temp = self:update_max_temp()
        self.vrd.VrdTemperatureCelsius = max_temp

        self:update_vrd_voltage()

        self:update_vrd_temp()
        self.vrd.Status = 0
    elseif cc == comp_code.CommandNotAvailable then
        self:reset_vrd_info()
    elseif self.vrd.Status == 0 and self.retry >= max_retry then
        log:debug('unable to get voltage and temperature in 3 times, set vrd values to invalid')
        self:set_vrd_to_invalid()
    end
end

return vrd_object
