-- 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: CPLD固件信息初始化与上树
local log = require 'mc.logging'
local mdb = require 'mc.mdb'
local skynet = require 'skynet'
local defs = require 'unit_manager.class.logic_fw.comm_defs'
local ctx = require 'mc.context'
local client = require 'general_hardware.client'
local chip_lock_singleton = require 'chip_lock'

local FWINVENTORY_PATH < const > = '/bmc/kepler/UpdateService/FirmwareInventory'
local FWINVENTORY_INTERFACE < const > = 'bmc.kepler.UpdateService.FirmwareInventory'
local MAX_RETRY = 10
local DEFAULT_VERSION = '0.00'

local fw_info = {}

fw_info.__index = fw_info

function fw_info:register_logic_fw_info()
    -- BCU的CPLD对象在分发时，暂不注册，对象分发完毕之后统一注册
    if string.match(self.id, "^BCU_CPLD") or string.match(self.id, "^EXU_CPLD") then
        log:notice("BCU/EXU CPLD FirmwareInventory obj is not registered")
        return
    end
    -- 注册CPLD固件信息到资源树
    skynet.fork(function()
        local ok, obj = pcall(mdb.get_object, self.sd_bus, FWINVENTORY_PATH, FWINVENTORY_INTERFACE)
        local times = 120 -- 重试120s, 防止资源树未加载
        while not ok and times > 0 do
            ok, obj = pcall(mdb.get_object, self.sd_bus, FWINVENTORY_PATH, FWINVENTORY_INTERFACE)
            skynet.sleep(100)
            times = times - 1
        end
        if not ok then
            log:error('get FirmwareInventory object failed')
            return
        end

        self.version = self:get_fw_version(defs.START_RYTRY_TIME)
        obj:Add(ctx.new(),
            {Id = self.id, Name = self.name, Version = self.version, BuildNum = self.build_num,
            ReleaseDate = self.release_date, LowestSupportedVersion = self.lowest_supported_ver,
            SoftwareId = self.software_id, Manufacturer = self.manufacturer, Location = self:get_fw_location(),
            State = 'Enabled', Severity = 'Informational'},
            self.updateable, self.capability, 10)   -- CPLD固件包当前最大不超过10MB

        log:notice("register %s version to FirmwareInventory finish, version: %s, location: %s",
            self.id, self.version, self:get_fw_location())
    end)
end

local function retry_add_logicfirmware(obj)
    local times = 120 -- 重试120s, 防止资源树未加载
    local ok = false
    while not ok and times > 0 do
        ok, _ = client:PFirmwareInventoryFirmwareInventoryAdd(ctx.new(),
            {Id = obj.id, Name = obj.name, Version = obj.version, BuildNum = obj.build_num,
            ReleaseDate = obj.release_date, LowestSupportedVersion = obj.lowest_supported_ver,
            SoftwareId = obj.software_id, Manufacturer = obj.manufacturer, Location = obj:get_fw_location(),
            State = 'Enabled', Severity = 'Informational'},
            obj.updateable, obj.capability, 10)
        skynet.sleep(100)
        times = times - 1
    end
end

-- 注册CPLD无感升级资源
function fw_info.register_Logic_fw(fw_list, unit_name)
    if not fw_list or type(fw_list) ~= "table" then
        return
    end
    skynet.fork(function ()
        log:notice("start to register %s cpld FirmwareInventory obj", unit_name)
        -- 如果只有一个就显示成XXX_CPLD，不加序号
        if #fw_list == 1 then
            local fw_obj = fw_list[1]
            fw_obj.version = fw_obj:get_fw_version(defs.START_RYTRY_TIME)
            fw_obj.id = unit_name .. "_CPLD_" .. fw_obj.position
            fw_obj.name = fw_obj.device_name .. " CPLD"
            retry_add_logicfirmware(fw_obj)
            log:notice("register %s_CPLD FirmwareInventory successfully", unit_name)
        elseif #fw_list > 1 then
            for _, obj in pairs(fw_list) do
                obj.version = obj:get_fw_version(defs.START_RYTRY_TIME)
                retry_add_logicfirmware(obj)
                log:notice("register %s FirmwareInventory successfully", obj.id)
            end
        end
    end)
end

function fw_info.unregister_cpld(cpld_fw_obj, obj_id)
    if not cpld_fw_obj then
        return
    end
    local ok, _ = client:PFirmwareInventoryFirmwareInventoryDelete(ctx.new(), obj_id)
    if ok then
        log:notice("[CPLD] unregister %s firmwareInventory success", obj_id)
        cpld_fw_obj.id = nil
    else
        log:error("[CPLD] unregister %s firmwareInventory fail", obj_id)
    end
end

local function fw_version_translator(version)
    if not version then
        log:warn("version is nil, use default")
        return DEFAULT_VERSION
    end
    -- version高4位为软件版本号高位，低4位为版本号低位
    return string.format("%d.%02d", version >> 4, version & 0xf)
end

local function fw_location_translator(location)
    if not location then
        log:warn("location is nil, use default")
        return "U0"
    end
    return string.format("U%u", location)
end

local function set_firmware_route(self, value)
    local ok, err
    for _ = 1, MAX_RETRY do
        ok, err = pcall(function ()
            self.csr.Routes = value
        end)
        if ok then
            return defs.RET.OK
        end
        skynet.sleep(50)
    end
    log:error('set Routes value %s failed, err: %s', value, err)
    return defs.RET.ERR
end

function fw_info:switch_to_default_route()
    -- 在升级中BMC与EXU直连所以firmware_route为0的对象不需要切换通路
    if self.firmware_route == 0 then
        return
    end
    if set_firmware_route(self, self.default_route) == defs.RET.ERR then
        return
    end
    -- 写CPLD或其他操作后,延时给硬件切换链路预留时间,轮询5次，每次500ms
    for i = 1, 5 do
        log:info("now route value%d[%d]", i, self.csr.Routes)
        skynet.sleep(50)
    end
end

function fw_info:switch_to_firmware_route()
    -- 在升级中BMC与EXU直连所以firmware_route为0的对象不需要切换通路
    if self.firmware_route == 0 then
        return
    end
    if set_firmware_route(self, self.firmware_route) == defs.RET.ERR then
        return
    end
    -- 写CPLD或其他操作后,延时给硬件切换链路预留时间,轮询5次，每次500ms
    for i = 1, 5 do
        log:info("now route value%d[%d]", i, self.csr.Routes)
        skynet.sleep(50)
    end
end

function fw_info:get_fw_version(retry_time)
    local retry_deley = retry_time * 100
    local ok, ver = pcall(function()
        local version = DEFAULT_VERSION
        for _ = 1, MAX_RETRY do
            -- 硬件获取可能为0，重试10次每次间隔10s。
            if self.csr.Version ~= 0 then
                version = self.csr.Version
                break
            end
            skynet.sleep(retry_deley)
        end
        return fw_version_translator(version)
    end)
    if not ok then
        log:error('get fw version failed, err: %s', ver)
        return DEFAULT_VERSION
    end
    log:notice('get fw version %s',ver)
    return ver
end

function fw_info:get_fw_location()
    return fw_location_translator(self.csr.Location)
end

-- 更新升级固件版本
function fw_info:check_update_ver()
    skynet.fork(function()
        -- 防护措施，cpld无感升级之后，等待100ms，再查版本
        skynet.sleep(10)
        local cur_ver = self:get_fw_version(defs.RYTRY_TIME)
        if self.version ~= cur_ver then
            log:notice('update FirmwareInfo(%s) version from %s to %s', self.id, self.version, cur_ver)
            client:ForeachFirmwareInfoObjects(function (obj)
                if obj.Id == self.id then
                    obj.Version = cur_ver
                    self.version = cur_ver
                    return
                end
            end)
        end
    end)
end

function fw_info:i2c_update_chip_lock(ctx, lock_time)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():lock(self.i2c_update_lock_chip, ctx, lock_time)
    end)
end

function fw_info:i2c_update_chip_unlock(ctx)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():unlock(self.i2c_update_lock_chip, ctx)
    end)
end

function fw_info:update_chip_lock(ctx, lock_time)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():lock(self.update_lock_chip, ctx, lock_time)
    end)
end

function fw_info:update_chip_unlock(ctx)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():unlock(self.update_lock_chip, ctx)
    end)
end

function fw_info.new(csr, pos, bus, device_name)
    local idx = string.match(csr.Name, 'CPLD(%d)') or ''
    local cpld_type = string.match(csr.Name, 'FPGA') and ' FPGA' or ' CPLD'
    local ok, sys_id = pcall(csr.get_system_id, csr)
    if not ok then
        log:error("get_system_id failed, err: %s", sys_id)
        sys_id = 1
    end
    log:notice('fw_info.new: get_system_id %s', sys_id)
    log:notice("obj UId:[%s] ComponentID:[%s] ComponentIDEx:[%s] idx:[%s] csr.Name:[%s] device_name:[%s]",
        csr.UId, csr.ComponentID, csr.ComponentIDEx, idx, csr.Name, device_name)
    return setmetatable({
        system_id = sys_id,
        id = csr.Name .. "_" .. pos,
        name = device_name and device_name .. cpld_type .. idx,
        manufacturer = csr.Manufacturer,
        uid = csr.UId,
        smc_chip = csr.SmcChip,
        i2c_upgrade_chip = csr.I2CUpgradeChip,
        i2c_update_lock_chip = csr.I2CUpgradeLockChip,
        update_chip = csr.UpgradeChip,
        update_lock_chip = csr.UpgradeLockChip,
        chip_info = csr.ChipInfo,
        default_route = csr.DefaultRoute,
        firmware_route = csr.FirmwareRoute,
        build_num = "",
        release_date = "",
        lowest_supported_ver = "",
        software_id = csr.SoftwareId and csr.SoftwareId or '',
        updateable = true,
        capability = 3, -- 参考V2 XML配置为软件分类 1:MGMT firmware(BMC或HMM或RMC),2:BIOS,3:CPLD,4:Driver,5:Uboot
        active_mode = defs.CPLD_ACTIVE_MODE.NO_IMMED,
        csr = csr, --访问accessor需要保存csr原始信息
        sd_bus = bus,
        position = pos,
        device_name = device_name,
        component_id = csr.ComponentID,
        component_id_ex = csr.ComponentIDEx,
        valid_mode = csr.ValidMode,
        valid_condition = csr.ValidCondition,
        sub_component_sku = csr.SubComponentSku
    }, fw_info)
end

return fw_info