-- 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 log = require 'mc.logging'
local file_sec = require 'utils.file'
local utils = require 'dpu_service.utils'
local mc_utils = require 'mc.utils'
local skynet = require 'skynet'
local chip_lock_singleton = require 'chip_lock'

local dpu_upgrade_object = {}
dpu_upgrade_object.__index = dpu_upgrade_object

function dpu_upgrade_object.new(obj)
    return setmetatable({
        dpu_object = obj,
        std_smbus = obj.std_smbus,
        lock_chip = obj.lock_chip
    },dpu_upgrade_object)
end

function dpu_upgrade_object:query_upgrade_status()
    local ok, payload = pcall(function()
        return self.std_smbus:GetMcuUpgradeStatus()
    end)
    if not ok then
        return
    end
    return payload.status
end

function dpu_upgrade_object:mcu_upgrade(fw_path, numerator, denominator, system_id, firmware_type, parameters)
    local ok, res
    for i = 1, 5 do
        ok, res = pcall(function ()
            return self:retry_mcu_upgrade(i, fw_path, numerator, denominator, system_id, firmware_type, parameters)
        end)
        if ok and res then
            return true
        end
        log:error('[DPU]upgrade DPU MCU failed, retry: %s, err: %s', i, res)
        skynet.sleep(100)
    end
    return false
end

function dpu_upgrade_object:retry_mcu_upgrade(retry, fw_path, numerator, denominator, system_id, firmware_type,
        parameters)
    log:notice('[DPU] start to upgrade Mcu')
    local fp = file_sec.open_s(fw_path, 'rb')
    if not fp then
        log:error('[DPU] open firmware file fail')
        return false
    end
    skynet.sleep(200) -- 防止安全解压后立即打开数据拿不到
    local fw_stream = mc_utils.close(fp, pcall(fp.read, fp, 'a'))

    -- Step 1: check the upgrade status is not in process
    local is_idle = utils.retry_func(100, 10, function()
        local state = self:query_upgrade_status()
        log:notice('[DPU]Step 1: query MCU upgrade status, status code %s', state)
        return state ~= nil and state ~= utils.STD_SMBUS_UPGRADE_STATUS.INPROCESS and state ~=
                utils.STD_SMBUS_UPGRADE_STATUS.INSYNC
    end)
    if not is_idle then
        log:error('[DPU] Mcu is busy, cannot upgrade')
        return false
    end

    -- Step 2: send the firmware to the device
    local send_file_success = utils.retry_func(100, 10, function()
        return self.std_smbus:UpgradeMcu(fw_stream, numerator, denominator, system_id, firmware_type, retry, parameters)
    end)
    if not send_file_success then
        log:error('[DPU] fail to send upgrade file to Mcu')
        return false
    end

    -- Step 3: check the device is upgrading
    local is_upgrading = utils.retry_func(100, 10, function()
        local state = self:query_upgrade_status()
        log:notice('[DPU]Step 3: query MCU upgrade status, status code %s', state)
        return state
    end)
    if is_upgrading ~= utils.STD_SMBUS_UPGRADE_STATUS.INPROCESS and is_upgrading ~=
        utils.STD_SMBUS_UPGRADE_STATUS.INSYNC then
        log:error('[DPU] Mcu is not upgrading, state:%s', is_upgrading)
        return false
    end

    -- Step 4: wait the device becomes idle
    local is_finished = utils.retry_func(100, 120, function()
        local state = self:query_upgrade_status()
        log:notice('[DPU]Step 4: query MCU upgrade status, status code %s', state)
        return state == utils.STD_SMBUS_UPGRADE_STATUS.IDLE
    end)
    if not is_finished then
        log:error('[DPU] Mcu upgrade cannot finish')
        return false
    end

    log:notice('[DPU] Mcu upgrade success')
    return true
end

local cpld_status <const> = {
    'idle',
    'flash is clearing',
    'upgrading',
    'upgrade success',
    'clear flash failed',
    'upgrade failed',
    'validing',
    'valid failed'
}

local can_upgrade_list <const> = {
    Idel = 0,
    UpgradeSuccess = 3,
    UpgradeFailed = 5,
    ValidFailed = 7
}

local upgrade_failed_list <const> = {
    ClearFlashFailed = 4,
    UpgradeFailed = 5,
    ValidFailed = 7
}

local vrd_can_upgrade_list <const> = {
    Idel = 0,
    UpgradeFailed = 3
}

local vrd_upgrade_fail_list <const> = {
    WriteFailed = 2,
    UpgradeFailed = 3
}

local function judge_cpld_status(status, list)
    for _, v in pairs(list) do
        if status == v then
            return true
        end
    end
    return false
end

function dpu_upgrade_object:query_cpld_upgrade_status(cpld_id)
    local ok, ret = pcall(function()
        return self.std_smbus:GetCpldUpgradeStatus(cpld_id - 1)
    end)
    if not ok then
        log:error('[DPU] CPLD%s can not upgrade cpld now, status: %s', cpld_id, ret)
        return
    end
    return ret.status
end

function dpu_upgrade_object:cpld_upgrade(fw_path, ...)
    log:notice('[DPU] start to upgrade sdi cpld')
    local numerator, denominator, system_id, firmware_type, parameters, cpld_id = ...
    -- Step 1: 查询cpld状态，确认是否可以升级
    local is_idle = utils.retry_func(100, 10, function()
        local status = self:query_cpld_upgrade_status(cpld_id)
        log:notice('[DPU] query CPLD%s upgrade status, status code %s', cpld_id, status)
        return status ~= nil and judge_cpld_status(status, can_upgrade_list)
    end)
    if not is_idle then
        log:error('[DPU] CPLD%s mcu is busy, cannot upgrade', cpld_id)
        return false
    end

    log:notice('[DPU] Step 1: make sure cpld%s can upgrade', cpld_id)
    -- Step 2: 发送升级文件
    local send_file_success = utils.retry_func(100, 10 ,function ()
        return self.std_smbus:UpgradeFirmware(
            fw_path, numerator, denominator, system_id, firmware_type, parameters, cpld_id)
    end)

    if not send_file_success then
        log:error('[DPU] CPLD%s fail to send upgrade file to Cpld', cpld_id)
        return false
    end
    log:notice('[DPU] Step 2: CPLD%s send upgrade file to Cpld successfully', cpld_id)

    -- Step 3: 确认升级状态,根据基线最多等待900s
    local upgrade_success = self:wait_upgrade_success(cpld_id)
    if not upgrade_success then
        log:error('[DPU] CPLD%s upgrade cpld overtime', cpld_id)
        return false
    end
    
    -- Step 4: 开始生效
    local valid_ok, err = utils.retry_func(100, 10, function ()
        return pcall(function ()
            return self.std_smbus:VaildCpld(cpld_id - 1)
        end)
    end)
    if not valid_ok then
        log:error('[DPU] CPLD%s start to vaild cpld failed, err:%s', cpld_id, err)
        return false
    end

    -- Step 5: 确认生效状态，根据基线最多等待900s
    local valid_success = self:wait_valid_success(cpld_id)
    if not valid_success then
        log:error('[DPU] valid cpld%s overtime', cpld_id)
        return false
    end
    log:notice('[DPU] Step 5: valid cpld%s with Mcu successfully', cpld_id)
    return true
end

function dpu_upgrade_object:wait_upgrade_success(cpld_id)
    for i = 1, 900 do
        local ok, ret = pcall(function ()
            return self.std_smbus:GetCpldUpgradeStatus(cpld_id - 1)
        end)
        -- 升级失败、擦除失败、生效失败直接退出
        if ok and judge_cpld_status(ret.status, upgrade_failed_list) then
            log:error('[DPU] CPLD%s upgrade cpld failed, status: %s', cpld_id, ret.status)
            return false
        end
        -- 升级成功继续
        if ok and ret.status == 3 then
            log:notice('[DPU] Step 3: CPLD%s upgrade cpld with Mcu successfully', cpld_id)
            return true
        end

        if i % 10 == 0 then
            log:notice('[DPU] CPLD%s check upgrade status 900 second, now: %s, upgrade_status: %s',
                cpld_id, i, ret.status)
        end

        -- 其它情况重试
        skynet.sleep(100)
    end

    return false
end

function dpu_upgrade_object:wait_valid_success(cpld_id)
    log:notice('[DPU] Step 4: CPLD%s start to valid cpld with Mcu', cpld_id)
    for i = 1, 900 do
        local ok, value = pcall(function ()
            return self.std_smbus:GetCpldUpgradeStatus(cpld_id - 1)
        end)

        -- 升级失败、擦除失败、生效失败直接退出
        if ok and judge_cpld_status(value.status, upgrade_failed_list) then
            log:error('[DPU] upgrade cpld%s failed, status: %s', cpld_id, value.status)
            return false
        end
        -- 确认是否空闲
        if ok and value.status == 0 then
            log:notice('[DPU] Step 5: upgrade clpd%s with Mcu successfully', cpld_id)
            return true
        end
        -- 其它情况重试
        skynet.sleep(100)
    end
    return false
end

function dpu_upgrade_object:get_vrd_upgrade_status()
    local ok, ret = pcall(function()
        return self.std_smbus:GetVrdUpgradeStatus()
    end)
    if not ok then
        log:error('[DPU] can not upgrade vrd now, status: %s', ret)
        return
    end
    return ret.status
end

function dpu_upgrade_object:query_vrd_upgrade_status(vrd_status_list)
    local up_state = utils.retry_func(1000, 10, function()
        local status = self:get_vrd_upgrade_status()
        log:notice('[DPU] query VRD upgrade status, status code %s', status)
        return status ~= nil and judge_cpld_status(status, vrd_status_list)
    end)
    if not up_state then
        return false
    end
    return true
end

function dpu_upgrade_object:vrd_upgrade(fw_path, dpu_index, dpu_count, system_id, firmware_type, parameters)
    log:notice('[DPU] start to upgrade sdi vrd')

    -- Step 1: 查询状态，确认是否可以升级
    if not self:query_vrd_upgrade_status(vrd_can_upgrade_list) then
        log:error('[DPU] Vrd is busy, cannot upgrade')
        return false
    end
    log:notice('[DPU] Step 1: make sure vrd can upgrade')

    -- Step 2: 发送升级文件
    local send_file_success = utils.retry_func(100, 10 ,function ()
        return self.std_smbus:UpgradeFirmware(fw_path, dpu_index, dpu_count, system_id, firmware_type, parameters)
    end)

    if not send_file_success then
        log:error('[DPU] fail to send upgrade file to vrd')
        return false
    end
    log:notice('[DPU] Step 2: send upgrade file to Vrd successfully')

    -- Step 3: 确认升级状态
    if self:query_vrd_upgrade_status(vrd_upgrade_fail_list) then
        log:error('[DPU] Vrd upgrade failed')
        return false
    end

    log:notice('[DPU] Step 3: vrd upgrade successfully')
    return true
end

function dpu_upgrade_object:chip_lock(ctx, lock_time)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():lock(self.lock_chip, ctx, lock_time)
    end)
end

function dpu_upgrade_object:chip_unlock(ctx)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():unlock(self.lock_chip, ctx)
    end)
end

function dpu_upgrade_object:upgrade(fw_path, dpu_index, dpu_count, system_id, firmware_type, parameters, upgrade_id)
    local upgrade = {
        ['Mcu'] = function()
            return self:mcu_upgrade(fw_path, dpu_index, dpu_count, system_id, firmware_type, parameters)
        end,
        ['DPUCpld'] = function()
            return self:cpld_upgrade(fw_path, dpu_index, dpu_count, system_id, firmware_type, parameters, upgrade_id)
        end,
        ['DPUVrd'] = function()
            return self:vrd_upgrade(fw_path, dpu_index, dpu_count, system_id, firmware_type, parameters)
        end
    }

    local lock_time_out = 600
    local ok, ret_code
    while lock_time_out > 0 do
        ok, ret_code = self:chip_lock(require 'mc.context'.new(), 600)
        if ok and ret_code == 0 then
            local upgrade_ret = upgrade[firmware_type]()
            -- 硬件总线解锁
            ok, ret_code = self:chip_unlock(require 'mc.context'.new())
            if not ok or ret_code ~= 0 then
                log:error("[dpu upgrade] dpu(index:%s) set lock status to 0 failed, ret code:%s", dpu_index, ret_code)
            end
            return upgrade_ret
        end
        log:debug("[dpu upgrade] get chip lock failed")
        lock_time_out = lock_time_out - 1
        skynet.sleep(100)
    end

    log:error("[dpu upgrade] upgrade failed because can not get chip lock, index:%s", dpu_index)
    return false
end

return dpu_upgrade_object