-- 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 log = require 'mc.logging'
local fw_def = require 'device_tree.adapters.power_mgmt.protocol.upgrade.fw_def'
local signal = require 'signal'
local file_sec = require 'utils.file'
local utils = require 'device_tree.adapters.power_mgmt.utils'
local c_tasks = require 'mc.orm.tasks'
local _, skynet = pcall(require, 'skynet')
local ctx = require 'mc.context'
local mdb = require 'mc.mdb'
local client = require 'power_mgmt.client'

local MAX_LOAD_POWER_INFO_RETRY_TIMES<const> = 3
local GET_RESULT_RETRY<const> = 6
local WAIT_MODULE_TIME<const> = 100
local RETRY_INTERVAL<const> = 500

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

local upgrade_dcdc_file = "PS_DCDC.bin"
local upgrade_pfc_file = "PS_PFC.bin"
local upgrade_qb_file = "PS_QB.bin"

local MODULE_NUM_MAX<const> = 2
local PS_SOFT_MAX_CNT<const> = 3

local psu_cmd = {
    DCDC_APP_ERC = 0x8001,
    DCDC_APP_START = 0x8000,
    DCDC_APP_MODULE = 0x8020,
    PFC_APP_ERC = 0x4001,
    PFC_APP_START = 0x4000,
    PFC_APP_MODULE = 0x4020,
    QB_APP_ERC = 0x2001,
    QB_APP_START = 0x2000,
    QB_APP_MODULE = 0x2020,
}

local PS_STANDARD_READ = {
    DC_VERSION_OFFSET  = 0xe4,
    PFC_VERSION_OFFSET = 0xe7,
    QB_VERSION_OFFSET = 0xf3
}

local pmbus_upgrade = class()

function pmbus_upgrade:ctor(call_back, para_tab, is_single_upgrade)
    self.call_back = call_back
    self.para_tab = para_tab
    self.is_single_upgrade = is_single_upgrade
end

--[[
    fru 信息为：
    power_type   电源格式类型
    soft_cnt     软件个数
    protocol_ver 协议版本
    frame_len    帧长
    soft_info    电源固件数组

    固件数组结构为：
    soft_id      软件id
    erase_time   固件擦除时间 
    write_waiting_time  每次写入间隔时间
    restart_time 重启时间
    module_num   模组个数（1个软件可能有多个模组）
]]
-- 打印fru信息，便于定位
local function log_fru_info(fru)
    log:notice("power fru: power_type = %s, protocol_ver = %s, frame_len = %s, soft_cnt = %s",
        fru.power_type, fru.protocol_ver, fru.frame_len, fru.soft_cnt)
    for i = 1, fru.soft_cnt do
        local soft = fru.soft_info[i]
        log:notice("soft_id = %s, erase_time = %s, write_waiting_time = %s, restart_time = %s, module_num = %s",
            soft.soft_id, soft.erase_time, soft.write_waiting_time, soft.restart_time, soft.module_num)
    end
end

-- 根据电源上报的eeprom信息解析需要升级的module个数，一个电源多种固件，每种固件包含多个module
local function get_need_upgrade_app_cnt(soft_load_info)
    local app_count = 0
    for i = 1, soft_load_info.soft_cnt do
        local module_num = soft_load_info.soft_info[i].module_num
        -- 若为0xFF,则默认为1
        if module_num == 0xFF then
            soft_load_info.soft_info[i].module_num = 1
            module_num = 1
        end

        -- 目前的电源app，最大有两个module，这里做限制，防止电源底层错误
        if module_num > MODULE_NUM_MAX then
            log:error("module num = %d, bigger than %d", module_num, MODULE_NUM_MAX)
            return fw_def.SUPPLY_1
        end
        app_count = app_count + soft_load_info.soft_info[i].module_num
    end
    return app_count
end

local function check_software_count(soft_count)
    if soft_count > PS_SOFT_MAX_CNT then
        log:error('software count is not support')
        return fw_def.SUPPLY_1
    end
end

-- 从bin包获取固件信息
local function standard_get_soft_load_info_from_bin_file(soft_load_info, idx, file_path)
    if #file_path > fw_def.PATH_MAX then
        log:error('File path is invalid.')
        return nil, nil, nil, fw_def.SUPPLY_1
    end
    -- 有两个电源固件的电源，升级只有一个电源固件的电源升级包的场景，返回电源升级包和电源型号不匹配
    local file = file_sec.open_s(file_path, 'r')
    if not file then
        log:error('open bin file failed.')
        return nil, nil, nil, fw_def.SUPPLY_6
    end

    return utils.safe_close_file(file, function()
        local data = file:read('a')
        local soft_id = data:byte(1)
        local sub_data = data:sub(9, 16)
        -- 防止文件为空, sub_data为空串，unpack失败
        if #sub_data == 0 then
            log:error("sub_data len is 0")
            return nil, nil, nil, fw_def.SUPPLY_1
        end
        local power_type = string.unpack('I8', sub_data)
        local len = file:seek('end')
        return data, { soft_id = soft_id, power_type = power_type }, len, fw_def.SUPPLY_0
    end)
end

local function standard_check_soft_load_info(soft_info_from_hardware, check_data, idx)
    -- soft id 要匹配
    if soft_info_from_hardware.soft_info[idx].soft_id ~= check_data.soft_id then
        log:error("HW soft ID 0x%x , BIN soft ID 0x%x", soft_info_from_hardware.soft_info[idx].soft_id,
            check_data.soft_id)
        return E_FAILED
    end
    if soft_info_from_hardware.power_type ~= check_data.power_type then
        log:error(
            "HW data power_type=0x%x, BIN data power_type=0x%x", soft_info_from_hardware.power_type,
            check_data.power_type
        )
        return E_FAILED
    end

    return E_OK
end

-- erase_standard_data 擦除固件信息
function pmbus_upgrade:erase_standard_data(soft_info, cmd)
    -- 获取擦除固件结果
    local load_app_ctrl_cmd_read = function ()
        local ok, resp
        for i = 1, GET_RESULT_RETRY do
            ok, resp = pcall(function()
                return self:load_app_ctrl_cmd_read(self.cmd.LOAD_CTRL)
            end)
            if ok then
                return resp, E_OK
            end
            c_tasks.get_instance():sleep_ms(RETRY_INTERVAL)
            log:debug("Read erase result failed, will read again. times = %d", i)
        end
        return nil, resp or E_FAILED
    end
    -- 擦除固件
    local func = function ()
        local ret = self:load_app_ctrl_cmd(self.cmd.LOAD_CTRL, cmd)
        if ret ~= E_OK then
            log:error("Erase app area failed, soft ID 0x%x, cmd = 0x%4x, ret = %s", soft_info.soft_id, cmd, ret)
            return E_FAILED
        end
        -- 擦除时间
        c_tasks.get_instance():sleep_ms(soft_info.erase_time)

        local data, err = load_app_ctrl_cmd_read()
        if err ~= E_OK then
            log:error(
                "Get erase app area result failed , soft ID 0x%x, retry times = %d, ret = %s", soft_info.soft_id,
                GET_RESULT_RETRY, err
            )
            return E_FAILED
        end

        if data == 0 then
            return E_OK
        end
        return E_FAILED
    end
    -- 擦除失败重试
    for i = 1, fw_def.ERASE_RETRY do
        local ret = func()
        if ret == E_OK then
            return E_OK
        end
        c_tasks.get_instance():sleep_ms(RETRY_INTERVAL)
        log:debug("Check erase result failed, will erase again. times = %d", i)
    end
    return E_FAILED
end

local erase_cmd = ({
    [fw_def.FIRMWARE.DCDC_SOFT] = psu_cmd.DCDC_APP_ERC,
    [fw_def.FIRMWARE.PFC_SOFT] = psu_cmd.PFC_APP_ERC,
    [fw_def.FIRMWARE.QB_SOFT] = psu_cmd.QB_APP_ERC
})

function pmbus_upgrade:erase_cmd_check(soft_info)
    local single_erase_cmd = erase_cmd[soft_info.soft_id]
    -- 擦除固件失败
    if self:erase_standard_data(soft_info, single_erase_cmd) ~= E_OK then 
        log:error('Erase app area failed, soft ID 0x%x, cmd = 0x%4x', soft_info.soft_id, single_erase_cmd)
        return E_FAILED
    end
    log:notice("Erase app area success, soft ID 0x%x, cmd = 0x%4x", soft_info.soft_id, single_erase_cmd)
    return E_OK
end

local function get_negotiate_frame_length_action(soft_id, frame_len)
    -- 完成协商
    local result = tonumber(soft_id)
    result = result << 0x08 | 0x10
    local frame_len_idx = ({
        [8] = 0,
        [16] = 1,
        [32] = 2,
        [64] = 3,
        [128] = 4,
        [255] = 5,
    })[frame_len]
    return result | frame_len_idx
end

-- 协商帧长度
function pmbus_upgrade:standard_pmbus_negotiate_frame_length(soft_info, frame_len)
    if soft_info.soft_id == fw_def.FIRMWARE.DCDC_SOFT or
        soft_info.soft_id == fw_def.FIRMWARE.PFC_SOFT then
        return E_OK
    end
    local cmd = get_negotiate_frame_length_action(soft_info.soft_id, frame_len)
    local ret = self:load_app_ctrl_cmd(0xFC, cmd)
    if ret ~= E_OK then
        log:error("Send negotiate cmd failed, soft_id: 0x%x, cmd = 0x%4x, ret = %s", soft_info.soft_id, cmd, ret)
        return E_FAILED
    end
    c_tasks.get_instance():sleep_ms(soft_info.write_waiting_time)
    -- 查询命令是否执行成功
    local ok, resp = pcall(function()
        return self:load_app_ctrl_cmd_read(0xFC)
    end)
    if not ok and resp ~= 0 then
        log:error('switch module fail, soft id: 0x%x, ret = %s', soft_info.soft_id, resp)
        return E_FAILED
    end
    return E_OK
end

-- 结束加载
local RESTART_CMD = {
    [fw_def.FIRMWARE.DCDC_SOFT] = psu_cmd.DCDC_APP_START,
    [fw_def.FIRMWARE.PFC_SOFT] = psu_cmd.PFC_APP_START,
    [fw_def.FIRMWARE.QB_SOFT] = psu_cmd.QB_APP_START,
}

function pmbus_upgrade:restart_app_check(soft_info)
    local single_cmd = RESTART_CMD[soft_info.soft_id]
    local ok, ret
    for retry_time = 1, fw_def.RESTART_APP_RETRY_TIMES do
        ok, ret = pcall(function()
            return self:load_app_ctrl_cmd(0xFC, single_cmd)
        end)
        if ok then
            break
        end
        log:notice("Restart app failed, times is %s.", retry_time)
    end
    if not ok then
        log:error('Restart app failed, soft id: 0x%x, cmd = 0x%4x, ret = %s', soft_info.soft_id, single_cmd, ret)
        return E_FAILED
    end
    return E_OK
end

-- 加载电源固件的DCDC或者PFC软件
function pmbus_upgrade:standard_load_app(soft_info, soft_info_from_bin, soft_info_len, frame_len, pro_percent)
    log:notice("start load app psu firmware")
    local res = self:erase_cmd_check(soft_info)
    if res ~= E_OK then
        return res
    end
    c_tasks.get_instance():sleep_ms(soft_info.restart_time)
    local block_num = soft_info_len // frame_len
    if soft_info_len % frame_len > 0 then
        log:error('check frame failed, buf_len: 0x%x, frame: 0x%x', soft_info_len, frame_len)
    end

    -- 协商帧长度，非DCDC和PFC类型就设置帧长度
    if self:standard_pmbus_negotiate_frame_length(soft_info, frame_len) ~= E_OK then
        log:error('negotiate frame length failed, soft_id: %#x', soft_info.soft_id)
    end

    log:notice("cal need to send block num is %s, frame_len is %s", block_num, frame_len)
    local cur_percent = self.call_back:get_comp_percent()
    log:notice("start to write each block num, write wait time is %sms", soft_info.write_waiting_time)
    local ok, resp = pcall (
        function ()
            self:load_soft_bin_data_to_chip(soft_info_from_bin, frame_len, block_num,
                soft_info.write_waiting_time)
        end
        )
    if not ok then
        log:error('Firmware load data failed. %s', resp)
        return E_FAILED
    end
    log:notice("finish to  write each block num")

    self.call_back:set_comp_percent(cur_percent + pro_percent, self.para_tab)
    res = self:restart_app_check(soft_info)
    if res ~= E_OK then
        return res
    end
    c_tasks.get_instance():sleep_ms(soft_info.restart_time * 1000)
    log:notice("send restart firmware cmd success, wait %ss", soft_info.restart_time)
    ok, resp = pcall(function()
        return self:load_app_ctrl_cmd_read(self.cmd.LOAD_CTRL)
    end)
    if not ok or resp ~= 0 then
        return E_FAILED
    end
    return self:inquiry_psu_version(soft_info)
end

function pmbus_upgrade:switch_app(data)
    local ok, resp
    for retry_time = 1, fw_def.SWITCH_APP_RETRY_TIMES do
        ok, resp = pcall(function()
            return self:load_app_ctrl_cmd(self.cmd.LOAD_CTRL, data)
        end)
        if ok then
            break
        end
        log:notice("Switch app failed, times is %s.", retry_time)
    end
    return ok, resp
end

-- 根据soft load info来加载电源固件模块app
function pmbus_upgrade:standard_load_module_app(soft_info, soft_info_from_bin, soft_info_len, app_percent, frame_len)
    for i = 1, soft_info.module_num do
        if self:standard_load_app(soft_info, soft_info_from_bin, soft_info_len, frame_len, app_percent) ~= E_OK then
            return E_FAILED
        end
        if i < soft_info.module_num then
            -- 下发FC切换通道,升级下一个模块软件
            local cmd = ({
                [fw_def.FIRMWARE.DCDC_SOFT] = psu_cmd.DCDC_APP_MODULE,
                [fw_def.FIRMWARE.PFC_SOFT] = psu_cmd.PFC_APP_MODULE,
                [fw_def.FIRMWARE.QB_SOFT] = psu_cmd.QB_APP_MODULE,
            })[soft_info.soft_id]
            local ok, resp = self:switch_app(cmd | i)
            if not ok then
                log:error('switch app failed, soft id: 0x%x, cmd = 0x%4x, ret = %s', soft_info.soft_id, cmd | i, resp)
                return E_FAILED
            end

            c_tasks.get_instance():sleep_ms(WAIT_MODULE_TIME)

            ok, resp = pcall(function()
                return self:load_app_ctrl_cmd_read(0xFC)
            end)
            if not ok and resp ~= 0 then
                log:error('switch module fail, soft id: 0x%x, ret = %s', soft_info.soft_id, resp)
                return E_FAILED
            end
        end
    end
    return E_OK
end

function pmbus_upgrade:cal_percent_by_count(app_count)
    local app_percent = 0
    if app_count == 0 then
        log:warn('module num is zero!')
    else
        app_percent = (self.call_back:get_step_progress() - self.call_back:get_comp_percent()) // app_count
    end
    return app_percent
end

local function get_power_state()
    local ok, obj_list = pcall(client.GetFruCtrlObjects, client)
    if not ok then
        log:error("get power state failed, ok %s obj_list %s", ok, obj_list)
        return nil
    end
    if not next(obj_list) then
        log:error("get power state failed, obj_list is nil")
        return nil
    end
    for _, obj in pairs(obj_list) do
        return obj.PowerState
    end
end

function pmbus_upgrade:check_powerstate_before_upgrade(soft_id)
    local power_state = get_power_state()
    log:notice("power_state %s IsSupportPowerOnUpgrade %s", power_state, self.is_support_power_on_upgrade)
    -- 判断是QB固件，且在单电源升级场景下，非下电状态时不升级该类型固件
    if soft_id == fw_def.FIRMWARE.QB_SOFT and self.is_single_upgrade and
        power_state ~= 'OFF' then
        log:error("single psu, os is powerd on, forbid upgrade qb(soft_id: %#x) firmware", soft_id)
        return false
    end
    -- 如果该槽位电源不支持上电升级，非下电状态时不升级
    if self.is_support_power_on_upgrade == false and power_state ~= 'OFF' then
        log:error("power is on, not support upgrade firmware, %s", self.is_support_power_on_upgrade)
        return false
    end

    return true
end

-- 根据soft load info来加载电源固件app
function pmbus_upgrade:standard_load_power_supply_app(soft_load_info, upgrade_path)
    local app_count = get_need_upgrade_app_cnt(soft_load_info)
    local ret = fw_def.SUPPLY_0
    local app_percent = self:cal_percent_by_count(app_count)
    check_software_count(soft_load_info.soft_cnt)
    for i = 1, soft_load_info.soft_cnt do
        local soft_id = soft_load_info.soft_info[i].soft_id
        log:notice("[Power][Upgrade]soft_id = %d", soft_id)
        local file_path = ({
            [fw_def.FIRMWARE.DCDC_SOFT] = upgrade_path .. upgrade_dcdc_file,
            [fw_def.FIRMWARE.PFC_SOFT] = upgrade_path .. upgrade_pfc_file,
            [fw_def.FIRMWARE.QB_SOFT] = upgrade_path .. upgrade_qb_file,
        })[soft_id]
        local cur_percent = self.call_back:get_comp_percent()
        log:notice("[Power][Upgrade]cur upgrade progress : %d", cur_percent)
        -- 从软件包中获取soft id和power type
        local soft_info_from_bin, check_data, soft_info_len, err =
            standard_get_soft_load_info_from_bin_file(soft_load_info, i, file_path)

        if err ~= fw_def.SUPPLY_0 then
            ret = err
            self.call_back:set_comp_percent(cur_percent + app_percent, self.para_tab)
            log:error("Get soft load info from file failed, err %s", err)
            goto continue
        end

        if standard_check_soft_load_info(soft_load_info, check_data, i) ~= E_OK then
            ret = fw_def.SUPPLY_6
            self.call_back:set_comp_percent(cur_percent + app_percent, self.para_tab)
            log:error("Check app firmware 0x%x failed", soft_load_info.soft_info[i].soft_id)
            goto continue
        end
        log:notice("check bin source is adapter to %s firmware, contains module num is %s",
            soft_load_info.soft_info[i].soft_id, soft_load_info.soft_info[i].module_num)
        -- 部分电源不支持上电升级，需要进行处理
        if self:check_powerstate_before_upgrade(soft_id) == false then
            ret = fw_def.SUPPLY_2
            self.call_back:set_comp_percent(cur_percent + app_percent, self.para_tab)
            log:error("os is powerd on, forbid upgrade firmware")
            goto continue
        end
        if self:standard_load_module_app(
            soft_load_info.soft_info[i], soft_info_from_bin, soft_info_len, app_percent,
            1 << soft_load_info.frame_len + 3) ~= E_OK then
            ret = fw_def.SUPPLY_1
            log:error("Load module app firmware 0x%x failed", soft_load_info.soft_info[i].soft_id)
            break
        end
        ::continue::
    end
    self.call_back:set_comp_percent(self.call_back:get_step_progress(), self.para_tab)
    return ret
end

function pmbus_upgrade:process(upgrade_path)
    local ok1, soft_load_info = self:standard_get_soft_load_info()
    if not ok1 then
        log:error('get soft load info failed, resp :%s', soft_load_info)
        return fw_def.SUPPLY_1
    end
    local ok2, resp = pcall(log_fru_info, soft_load_info)
    if not ok2 then
        log:error('log fru info failed, resp :%s', resp)
        return fw_def.SUPPLY_1
    end
    -- 根据fru信息加载固件
    local ret = self:standard_load_power_supply_app(soft_load_info, upgrade_path)
    if ret ~= fw_def.SUPPLY_0 then
        log:error('Load app firmware failed, standard_load_power_supply_app return %d', ret)
    end
    return ret
end

-- 读电源fru信息
function pmbus_upgrade:standard_get_soft_load_info()
    local ok, resp
    for retry_times = 1, MAX_LOAD_POWER_INFO_RETRY_TIMES do
        ok, resp = pcall(function()
            return self:get_soft_load_info()
        end)
        if ok then
            return ok, resp
        end
        log:error('read iic addr: 0x%x fail, try: %d', self.i2c_slot_addr, retry_times)
        -- 查询失败，延时100ms
        c_tasks.get_instance():sleep_ms(100)
    end
    return ok, resp
end

function pmbus_upgrade:load_soft_bin_data_to_chip(soft_info_from_bin, frame_len, block_num, wait_time)
    local context = ctx.new()
    -- 设置超时时间600s
    context.Timeout = 600
    self.psu_chip:PluginRequest(context, 'power_mgmt', 'load_soft_bin_data_by_plugins',
        skynet.packstring(self.slot_i2c_addr, frame_len, block_num, wait_time, soft_info_from_bin))
end

return function (protocol_obj)
    return setmetatable(pmbus_upgrade, {__index = protocol_obj})
end
