-- 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 device_object = require 'mc.mdb.device_tree.device_object'
local log = require 'mc.logging'
local utils = require 'device_tree.adapters.power_mgmt.utils'
local utils_core = require 'utils.core'
local ctx = require 'mc.context'
local debounce = require 'mc.debounce.debounce'

local E_OK = nil -- 函数执行成功返回nil
local E_FAILED = '' -- 空错误信息

local psu = device_object('OnePower')

local ClassPsuSlot<const> = 'PsuSlot'
local is_utf8 = utils_core.utf8_validate

local PS_HEALTH_EVENT<const> = {
    VOUT_STATUS   = 0, -- 输出电压状态
    IOUT_STATUS   = 1, -- 输出电流状态
    VIN_STATUS    = 2, -- 输入电压状态
    TEMPER_STATUS = 3, -- 电源温度传感器的状态
    FAN_STATUS    = 4, -- 电源风扇的状态
    CML_STATUS    = 5, -- 命令的状态
    MFR_STATUS    = 6, -- 厂商自定义的状态

    PS_MONITOR_END = 7
}

local DEBOUNCE<const> = {
    DBD_CONBIN = {
        CONTNUM_H = 3,
        CONTNUM_L = 3
    },
    DBD_CONT = {
        CONT_NUM = 3
    }
}

local function snake_to_camel(name)
    local camel_str = ''
    pcall(function()
        return string.gsub(name, '([^_]+)', function(a)
            camel_str = camel_str .. a:sub(1, 1):upper() .. a:sub(2, -1)
        end)
    end)
    return camel_str
end

local function get_pbit(val)
    return 1 << val
end

function psu:ctor()
    self.protocol = false
    self.psu_slot = false
    self.last_present = false
    self.ps_id = 0
    self.frudata = {}
    self.debounce = {}
end

local onepower_properties = {
    'Protocol',
    'PowerSupplyType',
    'Health',
    "Rate", -- 额定功率
    "FirmwareVersion", -- 电源版本
    "SerialNumber", -- 序列号
    "RatedCurrentAmps", -- 额定电流
    "ProductionDate", -- 生产日期/制造日期
    "Manufacturer", -- 厂商信息
    "Model", -- 电源型号
    'OutputPowerWatts',
    'InputCurrentAmps',
    'OutputCurrentAmps',
    'InputVoltage',
    'OutputVoltage',
    'EnvTemperatureCelsius',
    'PrimaryChipTemperatureCelsius',
    'SecondaryChipTemperatureCelsius',
    'InnerTemperatureCelsius',
    'InputVoltageStatus',
    'InputFrequencyHz',
    'TotalRunningHours',
    'AlarmStatus',
    'PreAlarmStatus',
    'ScanStatus',
    'InputPowerWatts',
    'WorkMode',
    'NormalAndRedundancySupported',
    'CommunicationStatus'
}

function psu:dtor()
    self:stop_tasks()
    self.psu_slot:clear_property_fetch_task()
    self.psu_slot:psm_monitor_stop()
    self.psu_slot.init_ok = false
end

function psu:register_psu_slot()
    local retry_times = 10
    -- 关联psu_slot
    local position = self:get_position()
    local psu_slots = self:get_dev_objects(ClassPsuSlot, position:sub(1, #position - 2)) -- 获取上一级csr的psu_slot对象
    for _ = 1, retry_times do
        for _, psu_slot in pairs(psu_slots) do
            if psu_slot.SlotNumber == self.SlotNumber then
                self.psu_slot = psu_slot
                return
            end
        end
        if self.psu_slot then
            return
        end
        self:sleep_ms(1000)
    end
    log:error('ps%d find psu_slot failed', self.ps_id)
end

function psu:register_methods()
    -- 电源升级
    self:declare_method('Upgrade', 'bmc.dev.psu.upgrade', 'Upgrade', function (...)
        return self:power_upgrade(...)
    end)
    self:declare_method('Cooling', 'bmc.dev.psu.cooling', 'SetFanRPM', function (...)
        self.psu_slot:set_psu_fan_rpm(...)
    end)
    self:declare_method('BlackBox', 'bmc.dev.psu.black_box', 'GetBlackBoxData', function ()
        return self.psu_slot:get_black_box_data()
    end)
    self:declare_method('PowerMode', 'bmc.dev.psu.power_mode', 'SetSleepMode', function (...)
        return self.psu_slot:set_sleep_mode(...)
    end)
    self:declare_method('PowerMode', 'bmc.dev.psu.power_mode', 'SetWorkMode', function (...)
        return self.psu_slot:set_work_mode(...)
    end)
    self:declare_method('Power', 'bmc.dev.psu', 'GetFruData', function (...)
        return self:update_fru_data(...)
    end)
    self:declare_method('Power', 'bmc.dev.psu', 'PowerAccessControl', function (...)
        return self:ps_access_control(...)
    end)
end

function psu:init()
    local run_probe = function ()
        self:fetch_property()
        self.psu_slot:fetch_power_supply_info(self['PhysicalInterface'])
        self:register_methods()
        self.psu_slot.init_ok = true
    end
    self.ps_id = self.SlotNumber
    self:init_debounce()
    self:register_psu_slot()
    if not self.psu_slot then
        self:next_tick(function ()
            self:register_psu_slot()
            run_probe()
        end)
    else
        run_probe()
    end
end

function psu:ps_access_control(start_flag)
    if start_flag then
        self.psu_slot:power_monitor_start()
    else
        self.psu_slot:psm_monitor_stop()
    end
end

function psu:set_resource(prop, value)
    if type(value) == 'string' and not is_utf8(value) then
        value = 'N/A'
    end
    self[prop] = value
end

function psu:is_present()
    return self.Presence == 1
end

function psu:fetch_property()
    for _, prop in ipairs(onepower_properties) do
        -- 注册回调，支不支持属性刷新由slot-chip决定
        local snake_str = utils.camel_to_snake(prop)
        self.psu_slot:fetch_property(snake_str, function (value)
            self:set_resource(prop, value)
        end)
        self.psu_slot:fetch_property(prop, function (value)
            self:set_resource(prop, value)
        end)
    end
    self.psu_slot:fetch_property('health_event', function (...)
        self:handle_health_event(...)
    end)
    self.psu_slot:fetch_property('sleep_mode', function (value)
        self.SleepMode = value
        if value == 'DeepSleep' then
            self.WorkMode = 'StandbySpare'
            self.DeepSleepEnabled = 1
        else
            self.DeepSleepEnabled = 0
        end
    end)
    self.psu_slot:fetch_property('part_number', function(value)
        self:handle_part_number(value)
    end)
    self.psu_slot:fetch_property('PartNumber', function(value)
        self:handle_part_number(value)
    end)
end

function psu:init_debounce()
    local health_event_debounce = {}
    for _, idx in pairs(PS_HEALTH_EVENT) do
        health_event_debounce[idx] = debounce['Cont'].new(DEBOUNCE.DBD_CONT.CONT_NUM, 0)
    end
    health_event_debounce[PS_HEALTH_EVENT.VIN_STATUS] =
                debounce['Cont'].new(DEBOUNCE.DBD_CONT.CONT_NUM, 32768)
    self.debounce = {
        health_event = health_event_debounce,
        health = debounce['Cont'].new(DEBOUNCE.DBD_CONT.CONT_NUM, 0),
        input_loss = debounce['Cont'].new(DEBOUNCE.DBD_CONT.CONT_NUM, 255),
        fan1 = debounce['ContBin'].new(DEBOUNCE.DBD_CONBIN.CONTNUM_H, DEBOUNCE.DBD_CONBIN.CONTNUM_L, 0),
        fan2 = debounce['ContBin'].new(DEBOUNCE.DBD_CONBIN.CONTNUM_H, DEBOUNCE.DBD_CONBIN.CONTNUM_L, 0),
        output_fail = debounce['ContBin'].new(
            DEBOUNCE.DBD_CONBIN.CONTNUM_H, DEBOUNCE.DBD_CONBIN.CONTNUM_L, 0),
        over_temp = debounce['ContBin'].new(
            DEBOUNCE.DBD_CONBIN.CONTNUM_H, DEBOUNCE.DBD_CONBIN.CONTNUM_L, 0),
    }
end

-- 刷新电子标签
function psu:update_fru_data(value)
    local ok, ret
    while not ok do
        ok, ret = pcall(function()
            return self.psu_slot:get_fru_data(value)
        end)
        self:sleep_ms(1000)
    end
    self.frudata = ret

    local prop = {}
    local value = {}
    for elabel_prop, val in pairs(self.frudata) do
        table.insert(prop, elabel_prop)
        table.insert(value, val)
    end

    -- 增加失败重试，在方法调用失败之后等待3s，重新发起更新，重试次数为100次
    local RETRY_TIMES = 100
    for _ = 1, RETRY_TIMES do
        ok, ret = pcall(function()
            return self.RefFrudata:Update(ctx.new(), prop, value)
        end)
        if ok then
            log:notice('PS%s set frudata successfully', self.ps_id)
            return
        end
    end
    log:error('PS%s set frudata failed, msg: %s', self.ps_id, ret)
end

function psu:handle_health_event(health_event)
    local failure = health_event.event & get_pbit(0)
    self.Failure = failure

    local input_loss = self.debounce.input_loss:get_debounced_val(health_event.event & get_pbit(4) > 0 and 1 or 0)
    if input_loss == 1 then
        -- 在停止功率扫描时，将bit0 置为0，通知能效功率扫描停止
        self.ScanStatus = self.ScanStatus & ~get_pbit(0)
    end
    self.LossOfInput = input_loss

    if health_event.fan_fault ~= nil then
        -- fan_fault:
        -- bit2: Fan 2 Speed Override
        -- bit4: Fan 2 Warning
        -- bit6: Fan 2 Fault
        local fan2_fault = self.debounce.fan2:get_debounced_val(health_event.fan_fault & get_pbit(6) > 0 and 1 or 0)
        self.Fan2Fault = fan2_fault
        -- bit3: Fan 1 Speed Override
        -- bit5: Fan 1 Warning
        -- bit7: Fan 1 Fault
        local fan1_fault = self.debounce.fan1:get_debounced_val(health_event.fan_fault & get_pbit(7) > 0 and 1 or 0)
        self.Fan1Fault = fan1_fault
    end

    if health_event.output_current_fault ~= nil then
        local output_current_fault = self.debounce.health_event[PS_HEALTH_EVENT.IOUT_STATUS]:get_debounced_val(
            health_event.output_current_fault)
        self.OutputCurrentFault = output_current_fault
    end
    if health_event.output_voltage_fault ~= nil then
        local output_voltage_fault = self.debounce.health_event[PS_HEALTH_EVENT.VOUT_STATUS]:get_debounced_val(
            health_event.output_voltage_fault)
        self.OutputVoltageFault = output_voltage_fault
    end
    if health_event.input_voltage_fault ~= nil then
        local input_voltage_fault = self.debounce.health_event[PS_HEALTH_EVENT.VIN_STATUS]:get_debounced_val(
            health_event.input_voltage_fault)
        self.InputVoltageFault = input_voltage_fault
    end
    if health_event.fan_fault ~= nil then
        local fan_fault = self.debounce.health_event[PS_HEALTH_EVENT.FAN_STATUS]:get_debounced_val(
            health_event.fan_fault)
        self.FanFault = fan_fault
    end
    if health_event.temper_fault ~= nil then
        local temper_fault = self.debounce.health_event[PS_HEALTH_EVENT.TEMPER_STATUS]:get_debounced_val(
            health_event.temper_fault)
        self.OverTemperature = temper_fault
    end
end

function psu:handle_part_number(value)
    if self.SerialNumber ~= '' then
        self.PartNumber = #self.SerialNumber == 20 and self.SerialNumber:sub(3, 10) or value
    else
        local ret, resp = self.psu_slot:get_dynamic_data('serial_number', 3)
        if ret == E_OK and resp then
            self.PartNumber = #resp == 20 and resp:sub(3, 10) or value
        end
    end
end

function psu:run_probe()
    self.psu_slot:power_monitor_start()
end

function psu:power_upgrade(...)
    self:set_resource('IsUpgrading', true)
    local ret = self.psu_slot:power_upgrade(...)
    self:set_resource('IsUpgrading', false)
    return ret
end

function psu:start()
    self:run_probe()
    self:listen('IsUpgrading', function ()
        self.psu_slot.is_upgrading = true
    end)
end

return psu
